├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc ├── default-conflict-markers.png ├── diff-view.png ├── history-view.png └── jj-diffconflicts.txt ├── lua └── jj-diffconflicts │ ├── health.lua │ └── init.lua ├── plugin └── jj_diffconflicts.lua ├── scripts ├── make-conflicts.sh └── run_tests.lua └── tests ├── data ├── base ├── fruits.txt ├── left ├── long_markers.txt ├── missing_newline_markers.txt ├── multiple_conflicts.txt └── right ├── screenshots ├── fruits_ui ├── long_markers_ui ├── missing_newline_ui ├── multiple_conflicts_ui ├── tests-test_jj_diffconflicts.lua---history-view---displays-UI ├── tests-test_jj_diffconflicts.lua---run---displays-an-error-when-no-valid-conflict └── tests-test_jj_diffconflicts.lua---run---does-not-work-with-wrong-marker-length ├── test_internal.lua └── test_jj_diffconflicts.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # vimdoc tags 2 | /doc/tags 3 | 4 | # test repository created by `make-conflicts.sh` 5 | /testrepo 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rafik Draoui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Available targets:" 3 | @echo "test Run tests" 4 | @echo "conflicts Generate example conflicts in test repository" 5 | 6 | test: 7 | @TEST=1 nvim --headless --noplugin -l scripts/run_tests.lua 8 | 9 | conflicts: 10 | @./scripts/make-conflicts.sh 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jj-diffconflicts 2 | 3 | `jj-diffconflicts` is a merge tool for the [Jujutsu] version control system that runs in [Neovim]. 4 | It provides a two-way diff interface to resolve merge conflicts. 5 | It is heavily inspired by Seth House's [diffconflicts] plugin, which provides the same functionality for the Git and Mercurial version control systems. 6 | 7 | ## Motivation 8 | 9 | For explanations about why a two-way diff based on conflict markers might be more effective than a standard 3-way diff that compares file content, you can consult the following resources: 10 | 11 | - The ["Why?" section][why] of the README of `whiteinge/diffconflicts`. 12 | - A [short video] demonstrating how it works, and how it helps honing on the relevant differences between the conflicted sides. 13 | - An [article] contrasting how different tools handle the same conflict. 14 | 15 | But perhaps the best way is to [try it yourself](#test-repository). 16 | 17 | ## Installation 18 | 19 | `jj-diffconflicts` requires Neovim v0.10.0 or above. 20 | It can be installed like any other Neovim plugin. 21 | 22 | It only supports conflicts that use the (default) "diff" conflict marker style (c.f. `ui.conflict-marker-style` configuration value). 23 | If you use the "git" conflict marker style, then the [diffconflicts] plugin should be able to handle them. 24 | 25 | ## Usage 26 | 27 | Documentation is available through `:help jj-diffconflicts`. 28 | 29 | ### Invoking from Neovim 30 | 31 | When a buffer with conflict markers is loaded, the merge resolution UI can be invoked through the `:JJDiffConflicts` command. 32 | This will open a two-way diff in vertical splits that highlights the changes between the two sides of the conflict. 33 | 34 | To resolve the conflict, edit the left side until it contains the desired changes. 35 | Then save it and exit Neovim with `:qa`. 36 | If you want to abort without resolving the conflict, exit Neovim with `:cq` instead. 37 | 38 | ### Invoking through `jj resolve` 39 | 40 | To configure as a merge tool in Jujutsu, add the following to your [Jujutsu configuration]: 41 | 42 | ```toml 43 | [merge-tools.diffconflicts] 44 | program = "nvim" 45 | merge-args = [ 46 | "-c", "let g:jj_diffconflicts_marker_length=$marker_length", 47 | "-c", "JJDiffConflicts!", "$output", "$base", "$left", "$right", 48 | ] 49 | merge-tool-edits-conflict-markers = true 50 | ``` 51 | 52 | It can then be invoked with `jj resolve --tool diffconflicts`. 53 | 54 | This uses the `:JJDiffConflicts!` variant of the command. 55 | It works the same way as the base command. 56 | But it also opens a history view in a separate tab that contains a 3-way diff between the two sides of the conflict and their common ancestor. 57 | This can be useful to better understand how the two sides of the conflicts diverged, and can help with deciding which changes to keep on the left side of the two-way diff. 58 | 59 | If you don't want to use the history view, you can instead set `merge-args` to `["-c", "JJDiffConflicts", "$output"]`. 60 | 61 | ## Test repository 62 | 63 | The `make-conflicts.sh` script creates a Jujutsu repository in the `testrepo` directory whose working copy has conflicted files. 64 | It can be used to try `jj-diffconflicts` (or any other merge tool). 65 | 66 | The first file is `fruits.txt`, which contains the merge conflict described in the [Conflicts] section of Jujutsu's documentation. 67 | 68 | The second file is `poem.txt`, which contains a tricky merge conflict. 69 | When resolving it, one should keep in mind the points from the [merge tools benchmarks] to judge its effectiveness. 70 | 71 | The third file is `long_markers.txt`, which the contains the merge conflict described in the [Long conflict markers] section of the Jujutsu's documentation. 72 | It can be used to test if the merge tool can handle markers of length higher than the default value. 73 | 74 | The fourth file is `multiple_conflicts.txt`, which contains two conflict sections. 75 | 76 | The fifth file is `missing_newline.txt`, which contains a conflict for which one side is missing a terminating newline character. 77 | 78 | ## Troubleshooting 79 | 80 | The plugin includes a health check to detect potential issues that would prevent it from functioning properly. 81 | It can be invoked with `:checkhealth jj-diffconflicts`. 82 | 83 | ## Limitations 84 | 85 | - It hasn't yet been used on a wide range of conflicts, so it's possible that it doesn't handle some situations very well. 86 | - It can only handle 2-sided conflicts (but this is also a limitation of `jj resolve`). 87 | - Jujutsu is still evolving, so future versions could bring changes that the plugin can't handle yet. 88 | For example, conflict markers were changed between v0.17 and v0.18. 89 | 90 | _Caveat emptor_ 91 | 92 | ## Screenshots 93 | 94 | ### Conflict resolution view 95 | 96 | ![Conflict resolution view](./doc/diff-view.png) 97 | 98 | ### History view 99 | 100 | ![History view](./doc/history-view.png) 101 | 102 | ### Default conflict markers (without plugin) 103 | 104 | ![Default conflict markers](./doc/default-conflict-markers.png) 105 | 106 | [article]: https://www.eseth.org/2020/mergetools.html 107 | [conflicts]: https://jj-vcs.github.io/jj/latest/conflicts/#conflict-markers 108 | [diffconflicts]: https://github.com/whiteinge/diffconflicts/ 109 | [jujutsu]: https://jj-vcs.github.io/jj/ 110 | [jujutsu configuration]: https://jj-vcs.github.io/jj/latest/config/ 111 | [long conflict markers]: https://jj-vcs.github.io/jj/latest/conflicts/#long-conflict-markers 112 | [merge tools benchmarks]: https://github.com/whiteinge/diffconflicts/blob/master/_utils/README.md#mergetool-benchmarks 113 | [neovim]: https://neovim.io/ 114 | [short video]: https://www.youtube.com/watch?v=Pxgl3Wtf78Y 115 | [why]: https://github.com/whiteinge/diffconflicts/#why 116 | -------------------------------------------------------------------------------- /doc/default-conflict-markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafikdraoui/jj-diffconflicts/8140e5295ef2008a947f1f374c2d71a5bc7e38a0/doc/default-conflict-markers.png -------------------------------------------------------------------------------- /doc/diff-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafikdraoui/jj-diffconflicts/8140e5295ef2008a947f1f374c2d71a5bc7e38a0/doc/diff-view.png -------------------------------------------------------------------------------- /doc/history-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rafikdraoui/jj-diffconflicts/8140e5295ef2008a947f1f374c2d71a5bc7e38a0/doc/history-view.png -------------------------------------------------------------------------------- /doc/jj-diffconflicts.txt: -------------------------------------------------------------------------------- 1 | *jj-diffconflicts* A conflict resolution merge tool for Jujutsu VCS 2 | 3 | This plugin converts a file containing Jujutsu conflict markers into a two-way 4 | diff for easier merge conflict resolution. It is heavily inspired by the 5 | `diffconflicts` plugin (https://github.com/whiteinge/diffconflicts). 6 | 7 | For more information, see the `README` file in the repository for the plugin 8 | at https://github.com/rafikdraoui/jj-diffconflicts. 9 | 10 | To configure this plugin as a merge tool in Jujutsu, add the following to your 11 | Jujutsu configuration: 12 | > 13 | [merge-tools.diffconflicts] 14 | program = "nvim" 15 | merge-args = [ 16 | "-c", "let g:jj_diffconflicts_marker_length=$marker_length", 17 | "-c", "JJDiffConflicts!", "$output", "$base", "$left", "$right", 18 | ] 19 | merge-tool-edits-conflict-markers = true 20 | < 21 | It can then be invoked with `jj resolve --tool diffconflicts`. 22 | 23 | If you don't want to use the history view, you can instead set `merge-args` to 24 | `["-c", "JJDiffConflicts", "$output"]`. 25 | 26 | ============================================================================== 27 | 28 | Command ~ 29 | 30 | :JJDiffConflicts[!] *:JJDiffConflicts* 31 | 32 | Convert a file containing Jujutsu conflict markers into a two-way diff. 33 | 34 | If the optional ! is used, then a history view is also opened in a new 35 | tab. It will contain the merge base and both versions of the conflicted 36 | files. This requires the `$base`, `$left`, and `$right` files being passed 37 | as arguments when opening Neovim as a merge tool. 38 | 39 | If a [count] is given, then it is used as the length of the conflict 40 | markers. This can be useful when running the command directly instead of 41 | invoking it through `jj resolve`. If no [count] is given, then the merge 42 | tool will use the default length of 7 to parse conflict markers. 43 | 44 | See also: |g:jj_diffconflicts_no_command| 45 | 46 | 47 | Options ~ 48 | 49 | g:jj_diffconflicts_jujutsu_version *g:jj_diffconflicts_jujutsu_version* 50 | 51 | Specify the version of Jujutsu that is in use. This can matter because 52 | different versions differ in how they generate conflict markers. 53 | 54 | If unset (the default), `jj-diffconflicts` will detect the version by 55 | executing `jj --version`. You can set this variable if running the `jj` 56 | binary is not desirable. 57 | 58 | Example: 59 | `let g:jj_diffconflicts_jujutsu_version='0.17.1'` 60 | 61 | g:jj_diffconflicts_no_command *g:jj_diffconflicts_no_command* 62 | 63 | Set this variable if you don't want the |:JJDiffConflicts| command to be 64 | automatically defined. When set, the main plugin function can instead be 65 | run with `require("jj-diffconflicts").run()` 66 | 67 | 68 | Health check ~ 69 | 70 | A |health| check is included to detect possible misconfigurations. It can be 71 | run with `:checkhealth jj-diffconflicts`. 72 | 73 | 74 | vim:tw=78:ts=8:ft=help:norl 75 | -------------------------------------------------------------------------------- /lua/jj-diffconflicts/health.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Health check for detecting potential issues with running the plugin. 4 | -- See `:help health-dev`. 5 | M.check = function() 6 | vim.health.start("jj-diffconflicts report") 7 | 8 | if vim.fn.has("nvim-0.10.0") == 1 then 9 | vim.health.ok(string.format("Neovim version: %s", vim.version())) 10 | else 11 | vim.health.error("Only Neovim 0.10+ is supported") 12 | end 13 | 14 | local marker_style_cmd = 15 | vim.system({ "jj", "config", "get", "ui.conflict-marker-style" }):wait() 16 | if marker_style_cmd.code ~= 0 then 17 | vim.health.error( 18 | "Could not get conflict-marker-style config: " .. marker_style_cmd.stderr 19 | ) 20 | else 21 | local marker_style = vim.trim(marker_style_cmd.stdout) 22 | if marker_style ~= "diff" then 23 | vim.health.error("Unsupported ui.conflict-marker-style: " .. marker_style) 24 | else 25 | vim.health.ok("ui.conflict-marker-style: " .. marker_style) 26 | end 27 | end 28 | 29 | local ok, version = pcall(require("jj-diffconflicts").get_jj_version) 30 | if not ok then 31 | vim.health.error("Could not get Jujutsu version: " .. version) 32 | else 33 | vim.health.info(string.format("Detected Jujutsu version: %s", version)) 34 | end 35 | 36 | local version_override = vim.g.jj_diffconflicts_jujutsu_version 37 | if version_override == nil then 38 | vim.health.info("g:jj_diffconflicts_jujutsu_version is unset") 39 | else 40 | local msg = string.format("g:jj_diffconflicts_jujutsu_version = %q", version_override) 41 | local ok = pcall(vim.version.parse, version_override) 42 | if ok then 43 | vim.health.info(msg) 44 | else 45 | vim.health.error(msg) 46 | end 47 | end 48 | end 49 | 50 | return M 51 | -------------------------------------------------------------------------------- /lua/jj-diffconflicts/init.lua: -------------------------------------------------------------------------------- 1 | -- For more information about what this code is doing, refer to the README. 2 | -- 3 | -- For an explanation about the terminology used in code comments to describe 4 | -- conflict markers ("snapshot", "diff section", etc.), refer to 5 | -- https://jj-vcs.github.io/jj/latest/conflicts/#conflict-markers 6 | 7 | local M = {} 8 | local h = {} 9 | 10 | -- Public functions ----------------------------------------------------------- 11 | 12 | -- Convert a file containing Jujutsu conflict markers into a two-way diff 13 | -- conflict resolution UI. If the `show_history` argument is true, then it also 14 | -- includes a history view that displays the two sides of the conflict and 15 | -- their ancestor. 16 | M.run = function(show_history, marker_length) 17 | local ok, jj_version = pcall(M.get_jj_version) 18 | if not ok then 19 | vim.notify( 20 | "jj-diffconflicts: could not get jujutsu version, assuming latest version", 21 | vim.log.levels.ERROR 22 | ) 23 | jj_version = { math.huge, math.huge, math.huge } 24 | end 25 | 26 | if marker_length == 0 or marker_length == nil then 27 | marker_length = vim.g.jj_diffconflicts_marker_length 28 | if marker_length == "" or marker_length == nil then 29 | marker_length = 7 30 | end 31 | end 32 | local patterns = h.get_patterns(jj_version, marker_length) 33 | 34 | local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) 35 | local ok, raw_conflicts = pcall(h.extract_conflicts, patterns, lines) 36 | if not ok then 37 | vim.notify( 38 | "jj-diffconflicts: extract conflicts: " .. raw_conflicts, 39 | vim.log.levels.ERROR 40 | ) 41 | return 42 | end 43 | if vim.tbl_isempty(raw_conflicts) then 44 | vim.notify("jj-diffconflicts: no conflicts found in buffer", vim.log.levels.WARN) 45 | return 46 | end 47 | 48 | local conflicts = {} 49 | for _, raw_conflict in ipairs(raw_conflicts) do 50 | local ok, conflict = pcall(h.parse_conflict, patterns, raw_conflict) 51 | if not ok then 52 | vim.notify("jj-diffconflicts: parse conflict: " .. conflict, vim.log.levels.ERROR) 53 | return 54 | end 55 | table.insert(conflicts, conflict) 56 | end 57 | 58 | h.setup_ui(conflicts, show_history) 59 | end 60 | 61 | -- Return a table representing a software version that can be used as an 62 | -- argument to `vim.version.cmp`. 63 | -- 64 | -- If the `g:jj_diffconflicts_jujutsu_version` variable is set, then it will be 65 | -- used as the version. Otherwise we run the `jj` binary to find its version. 66 | -- 67 | -- The function is exported because it is used by the plugin health check. 68 | M.get_jj_version = function() 69 | if vim.g.jj_diffconflicts_jujutsu_version ~= nil then 70 | -- Escape hatch if running `jj` binary is not desirable 71 | return vim.version.parse(vim.g.jj_diffconflicts_jujutsu_version) 72 | end 73 | 74 | local version_cmd = vim.system({ "jj", "--version" }):wait() 75 | if version_cmd.code ~= 0 then 76 | -- Only keep first line of error message 77 | h.err(vim.split(version_cmd.stderr, "\n")[1]) 78 | end 79 | 80 | return vim.version.parse(version_cmd.stdout) 81 | end 82 | 83 | -- Helpers -------------------------------------------------------------------- 84 | 85 | -- Define regular expression patterns to be used to detect conflict markers. We 86 | -- cannot just define them as constants, since they can vary based on Jujutsu's 87 | -- version or provided marker length. 88 | h.get_patterns = function(jj_version, marker_length) 89 | vim.validate({ 90 | marker_length = { 91 | marker_length, 92 | function(arg) return type(arg) == "number" and arg > 0 end, 93 | "positive number", 94 | }, 95 | }) 96 | 97 | local marker = { 98 | top = string.rep("<", marker_length), 99 | bottom = string.rep(">", marker_length), 100 | diff = string.rep("%", marker_length), 101 | snapshot = string.rep("+", marker_length), 102 | } 103 | 104 | if vim.version.lt(jj_version, { 0, 18, 0 }) then 105 | -- Versions prior to v0.18.0 don't include trailing explanations 106 | return { 107 | top = "^" .. marker.top .. "$", 108 | bottom = "^" .. marker.bottom .. "$", 109 | -- We need to double `marker.diff` to escape the `%` symbols 110 | diff = "^" .. marker.diff .. marker.diff .. "$", 111 | snapshot = "^" .. marker.snapshot .. "$", 112 | } 113 | else 114 | return { 115 | top = "^" .. marker.top .. " .+$", 116 | bottom = "^" .. marker.bottom .. " .+$", 117 | -- We need to double `marker.diff` to escape the `%` symbols 118 | diff = "^" .. marker.diff .. marker.diff .. " .+$", 119 | snapshot = "^" .. marker.snapshot .. " .+$", 120 | } 121 | end 122 | end 123 | 124 | -- Extract conflict sections from the buffer. 125 | -- Return a list of objects with the raw contents of the conflicts sections, 126 | -- along with the line numbers corresponding to their top and bottom. 127 | -- 128 | -- For example, given the following buffer content: 129 | -- 130 | -- 1| Fruits: 131 | -- 2| <<<<<<< Conflict 1 of 1 132 | -- 3| %%%%%%% Changes from base to side #1 133 | -- 4| apple 134 | -- 5| -grape 135 | -- 6| +grapefruit 136 | -- 7| orange 137 | -- 8| +++++++ Contents of side #2 138 | -- 9| APPLE 139 | -- 10| GRAPE 140 | -- 11| ORANGE 141 | -- 12| >>>>>>> Conflict 1 of 1 ends 142 | -- 143 | -- Then the following will be returned: 144 | -- { 145 | -- { 146 | -- top = 2, 147 | -- bottom = 12, 148 | -- lines = { 149 | -- "%%%%%%% Changes from base to side #1", 150 | -- " apple", "-grape", "+grapefruit", " orange", 151 | -- "+++++++ Contents of side #2", 152 | -- "APPLE", "GRAPE", "ORANGE", 153 | -- }, 154 | -- } 155 | -- } 156 | h.extract_conflicts = function(patterns, buffer_lines) 157 | local conflicts = {} 158 | local lnum = 1 159 | local max_lnum = #buffer_lines 160 | while lnum <= max_lnum do 161 | local line = buffer_lines[lnum] 162 | if string.find(line, patterns.top) then 163 | -- We're at the start of a conflict section, iterate through the next 164 | -- lines until we find the end of the conflict. 165 | local conflict_top = lnum 166 | local bottom_found = false 167 | lnum = lnum + 1 168 | while lnum <= max_lnum and not bottom_found do 169 | line = buffer_lines[lnum] 170 | if not string.find(line, patterns.bottom) then 171 | -- Still inside conflict, continue onwards to next line 172 | lnum = lnum + 1 173 | else 174 | -- We found the bottom. Extract lines between top and bottom markers 175 | -- (excluding them) and save them for the return value. 176 | bottom_found = true 177 | local conflict_bottom = lnum 178 | local conflict_lines = 179 | vim.list_slice(buffer_lines, conflict_top + 1, conflict_bottom - 1) 180 | 181 | h.validate_conflict(patterns, conflict_lines) 182 | table.insert(conflicts, { 183 | top = conflict_top, 184 | bottom = conflict_bottom, 185 | lines = conflict_lines, 186 | }) 187 | end 188 | end 189 | if not bottom_found then 190 | h.err( 191 | string.format( 192 | "could not find bottom marker matching %q", 193 | buffer_lines[conflict_top] 194 | ) 195 | ) 196 | end 197 | end 198 | lnum = lnum + 1 199 | end 200 | return conflicts 201 | end 202 | 203 | -- Validate that the expected conflict sections are present 204 | h.validate_conflict = function(patterns, lines) 205 | local num_diffs = 0 206 | local has_snapshot = false 207 | for _, l in ipairs(lines) do 208 | if string.find(l, patterns.diff) then 209 | num_diffs = num_diffs + 1 210 | elseif string.find(l, patterns.snapshot) then 211 | has_snapshot = true 212 | end 213 | end 214 | if num_diffs == 0 then 215 | h.err("could not find diff section of conflict") 216 | end 217 | if num_diffs > 1 then 218 | h.err( 219 | string.format("conflict has %d sides, at most 2 sides are supported", num_diffs + 1) 220 | ) 221 | end 222 | if not has_snapshot then 223 | h.err("could not find snapshot section of conflict") 224 | end 225 | end 226 | 227 | -- Parse raw lines of conflict marker into the "left", and "right" sections 228 | -- required to display the diff UI. 229 | -- 230 | -- For example, given the following input: 231 | -- { 232 | -- top = 2, 233 | -- bottom = 12, 234 | -- lines = { 235 | -- "%%%%%%%", " apple", "-grape", "+grapefruit", " orange", 236 | -- "+++++++", "APPLE", "GRAPE", "ORANGE", 237 | -- }, 238 | -- } 239 | -- 240 | -- Then the following will be returned: 241 | -- { 242 | -- left_side = { "apple", "grapefruit", "orange" }, 243 | -- right_side = { "APPLE", "GRAPE", "ORANGE" }, 244 | -- top_line = 2, 245 | -- bottom_line = 12, 246 | -- } 247 | h.parse_conflict = function(patterns, raw_conflict) 248 | local lines = raw_conflict.lines 249 | local raw_diff = nil 250 | local snapshot = nil 251 | 252 | local section_header = lines[1] 253 | if string.find(section_header, patterns.diff) then 254 | -- diff followed by snapshot 255 | local i = h.find_index(patterns.snapshot, lines) 256 | raw_diff = vim.list_slice(lines, 2, i - 1) 257 | snapshot = vim.list_slice(lines, i + 1, #lines) 258 | elseif string.find(section_header, patterns.snapshot) then 259 | -- snapshot followed by diff 260 | local i = h.find_index(patterns.diff, lines) 261 | snapshot = vim.list_slice(lines, 2, i - 1) 262 | raw_diff = vim.list_slice(lines, i + 1, #lines) 263 | else 264 | h.err("unexpected start for conflict: " .. section_header) 265 | end 266 | 267 | local diff = h.parse_diff(raw_diff) 268 | 269 | return { 270 | left_side = diff.new, 271 | right_side = snapshot, 272 | top_line = raw_conflict.top, 273 | bottom_line = raw_conflict.bottom, 274 | } 275 | end 276 | 277 | h.setup_ui = function(conflicts, show_history) 278 | if show_history then 279 | -- Set up history view in a separate tab 280 | vim.cmd.tabnew() 281 | xpcall(h.setup_history_view, function(err) 282 | vim.cmd.tabclose() 283 | vim.notify("jj-diffconflicts: setup history view: " .. err, vim.log.levels.ERROR) 284 | end) 285 | vim.cmd.tabnext(1) 286 | end 287 | 288 | -- Set up conflict resolution diff 289 | h.setup_diff_splits(conflicts) 290 | 291 | -- Display usage message 292 | vim.cmd.redraw() 293 | vim.notify( 294 | "Resolve conflicts leftward then save. Use :cq to abort.", 295 | vim.log.levels.WARN 296 | ) 297 | end 298 | 299 | -- Set up a two-way diff for conflict resolution. 300 | -- 301 | -- Both sides have the contents of the conflicted file, except that the 302 | -- materialized conflict (i.e. the section between conflict markers) is 303 | -- replaced by the "new" version of the diff on the left, and the snapshot on 304 | -- the right. 305 | h.setup_diff_splits = function(conflicts) 306 | local conflicted_content = vim.api.nvim_buf_get_lines(0, 0, -1, false) 307 | 308 | -- Set up right-hand side. 309 | vim.cmd.vsplit({ mods = { split = "belowright" } }) 310 | vim.cmd.enew() 311 | local right_side = h.get_content_for_side("right_side", conflicts, conflicted_content) 312 | vim.api.nvim_buf_set_lines(0, 0, -1, false, right_side) 313 | vim.cmd.file("snapshot") 314 | vim.cmd([[setlocal nomodifiable readonly buftype=nofile bufhidden=delete nobuflisted]]) 315 | vim.cmd.diffthis() 316 | 317 | -- Set up left-hand side 318 | vim.cmd.wincmd("p") 319 | local left_side = h.get_content_for_side("left_side", conflicts, conflicted_content) 320 | vim.api.nvim_buf_set_lines(0, 0, -1, false, left_side) 321 | vim.cmd.diffthis() 322 | 323 | -- Ensure diff highlighting is up to date 324 | vim.cmd.diffupdate() 325 | 326 | -- Put cursor at the top of the first conflict section 327 | vim.fn.cursor(conflicts[1].top_line, 1) 328 | end 329 | 330 | -- Given a side (one of "left_side" or "right_side"), the full content of the 331 | -- conflicted buffer (as a list of lines), and a list of conflicts, return the 332 | -- content (as a list of lines) that should be displayed for that side. 333 | h.get_content_for_side = function(side, conflicts, conflicted_content) 334 | for _, conflict in ipairs(conflicts) do 335 | -- Pad the content of the side with null values so that it has the same 336 | -- number of lines as the materialized conflict with markers. 337 | -- This enables us to replace the conflicts in `conflicted_content` by the 338 | -- (shorter) "side content" without shifting the indices (and thus getting 339 | -- out of sync with the line numbers in `conflict.{top,bottom}_line`). 340 | local span = conflict.bottom_line - conflict.top_line + 1 341 | local content_lines = conflict[side] 342 | local padding_lines = vim.fn["repeat"]({ vim.NIL }, span - #content_lines) 343 | vim.list_extend(content_lines, padding_lines) 344 | 345 | -- Replace materialized conflict with the padded "side content" 346 | for i, line in ipairs(content_lines) do 347 | conflicted_content[i + conflict.top_line - 1] = line 348 | end 349 | end 350 | 351 | -- Filter out padding from result 352 | return vim.tbl_filter(function(x) return x ~= vim.NIL end, conflicted_content) 353 | end 354 | 355 | -- Display the merge base alongside full copies of the "left" and "right" side 356 | -- of the conflict. This can help giving more context about the intent of the 357 | -- changes on each side. 358 | h.setup_history_view = function() 359 | local load_history_split = function(name) 360 | vim.cmd.buffer(name) -- open buffer whose name matches the given `name` 361 | vim.cmd.file(name) -- set the file name 362 | vim.cmd([[setlocal statusline=%t]]) -- only display the file name in status line 363 | vim.cmd([[setlocal nomodifiable readonly]]) 364 | vim.cmd.diffthis() 365 | end 366 | 367 | -- Open three vertical splits, and put cursor in left-most one 368 | vim.cmd.vsplit() 369 | vim.cmd.vsplit() 370 | vim.cmd.wincmd("h") 371 | vim.cmd.wincmd("h") 372 | 373 | -- Fill left-most split with content of `$left` 374 | load_history_split("left") 375 | 376 | -- Fill middle split with content of `$base` (i.e. the original text before 377 | -- the two sides diverged) 378 | vim.cmd.wincmd("l") 379 | load_history_split("base") 380 | 381 | -- Fill right-most split with content of `$right` 382 | vim.cmd.wincmd("l") 383 | load_history_split("right") 384 | 385 | -- Put cursor back in middle split 386 | vim.cmd.wincmd("h") 387 | end 388 | 389 | -- Parse the diff section into the "old" and "new" versions. 390 | h.parse_diff = function(diff) 391 | local old, new = {}, {} 392 | for _, line in ipairs(diff) do 393 | local symbol, rest = string.sub(line, 1, 1), string.sub(line, 2, -1) 394 | if symbol == "+" then 395 | table.insert(new, rest) 396 | elseif symbol == "-" then 397 | table.insert(old, rest) 398 | elseif symbol == " " then 399 | table.insert(old, rest) 400 | table.insert(new, rest) 401 | else 402 | h.err(string.format("unexpected diff line: %q", line)) 403 | end 404 | end 405 | 406 | return { old = old, new = new } 407 | end 408 | 409 | -- Return the index of the first item matching `pattern` in the given list. 410 | -- Raise an error if none can be found. 411 | h.find_index = function(pattern, list) 412 | for i, x in ipairs(list) do 413 | if string.find(x, pattern) then 414 | return i 415 | end 416 | end 417 | h.err(string.format("could not find element matching pattern %q", pattern)) 418 | end 419 | 420 | h.err = function(msg) error(msg, 0) end 421 | 422 | if vim.env.TEST ~= nil then 423 | -- Export internal functions when running tests 424 | for k, v in pairs(h) do 425 | M[k] = v 426 | end 427 | end 428 | 429 | return M 430 | -------------------------------------------------------------------------------- /plugin/jj_diffconflicts.lua: -------------------------------------------------------------------------------- 1 | if vim.g.jj_diffconflicts_no_command == nil then 2 | vim.api.nvim_create_user_command("JJDiffConflicts", function(opts) 3 | local show_history = opts.bang 4 | local marker_length = opts.count 5 | require("jj-diffconflicts").run(show_history, marker_length) 6 | end, { 7 | desc = "Resolve Jujutsu merge conflicts", 8 | bang = true, -- used to enable "history" view 9 | count = true, -- used to supply marker length 10 | }) 11 | end 12 | -------------------------------------------------------------------------------- /scripts/make-conflicts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This creates a Jujutsu repository in the `testrepo` directory whose working 3 | # copy has two conflicted files. It can be used to try `jj-diffconflicts` (or 4 | # any other merge tool). 5 | # 6 | # Adapted from https://github.com/whiteinge/diffconflicts/blob/master/_utils/make-conflicts.sh 7 | 8 | # Enable running a different `jj` binary by setting the $JJ environment variable 9 | JJ=${JJ:-jj} 10 | 11 | # Initialize new repository 12 | rm -rf testrepo 13 | ${JJ} git init testrepo 14 | cd testrepo || exit 1 15 | 16 | # Create initial revision 17 | cat <poem.txt 18 | twas bri1lig, and the slithy toves 19 | did gyre and gimble in the wabe 20 | all mimsy were the borogroves 21 | and the m0me raths outgabe. 22 | 23 | "Beware the Jabberwock, my son! 24 | The jaws that bite, the claws that catch! 25 | Beware the Jub jub bird, and shun 26 | The frumious bandersnatch!" 27 | EOF 28 | cat <fruits.txt 29 | apple 30 | grape 31 | orange 32 | EOF 33 | cat <long_markers.txt 34 | Heading 35 | ======= 36 | EOF 37 | cat <multiple_conflicts.txt 38 | X 39 | X 40 | X 41 | apple 42 | grape 43 | orange 44 | X 45 | X 46 | X 47 | X 48 | X 49 | apple 50 | grape 51 | orange 52 | EOF 53 | printf "grape" >missing_newline.txt 54 | ${JJ} bookmark create -r @ base 55 | ${JJ} commit -m 'Initial revision' 56 | 57 | # Create one side of the conflict 58 | cat <poem.txt 59 | 'Twas brillig, and the slithy toves 60 | Did gyre and gimble in the wabe: 61 | All mimsy were the borogroves 62 | And the mome raths outgabe. 63 | 64 | "Beware the Jabberwock, my son! 65 | The jaws that bite, the claws that catch! 66 | Beware the Jub jub bird, and shun 67 | The frumious bandersnatch!" 68 | EOF 69 | cat <fruits.txt 70 | apple 71 | grapefruit 72 | orange 73 | EOF 74 | cat <long_markers.txt 75 | HEADING 76 | ======= 77 | EOF 78 | cat <multiple_conflicts.txt 79 | X 80 | X 81 | X 82 | apple 83 | grapefruit 84 | orange 85 | X 86 | X 87 | X 88 | X 89 | X 90 | apple 91 | grape 92 | blood orange 93 | EOF 94 | printf "grapefruit" >missing_newline.txt 95 | ${JJ} bookmark create -r @ left 96 | ${JJ} describe -m 'Fix syntax mistakes, eat grapefruit' 97 | 98 | # Create the other side of the conflict 99 | ${JJ} new base 100 | cat <poem.txt 101 | twas brillig, and the slithy toves 102 | Did gyre and gimble in the wabe: 103 | all mimsy were the borogoves, 104 | And the mome raths outgrabe. 105 | 106 | "Beware the Jabberwock, my son! 107 | The jaws that bite, the claws that catch! 108 | Beware the Jubjub bird, and shun 109 | The frumious Bandersnatch!" 110 | EOF 111 | cat <fruits.txt 112 | APPLE 113 | GRAPE 114 | ORANGE 115 | EOF 116 | cat <long_markers.txt 117 | New Heading 118 | =========== 119 | EOF 120 | cat <multiple_conflicts.txt 121 | X 122 | X 123 | X 124 | APPLE 125 | GRAPE 126 | ORANGE 127 | X 128 | X 129 | X 130 | X 131 | X 132 | APPLE 133 | GRAPE 134 | ORANGE 135 | EOF 136 | printf "grape\n" >missing_newline.txt 137 | ${JJ} bookmark create -r @ right 138 | ${JJ} describe -m 'Fix syntax mistakes, ALL CAPS fruits' 139 | 140 | # Create a new (conflicted) change from both sides 141 | ${JJ} new left right -m 'Merge left and right' 142 | -------------------------------------------------------------------------------- /scripts/run_tests.lua: -------------------------------------------------------------------------------- 1 | if not vim.env.MINI_NVIM_PATH then 2 | vim.print("The $MINI_NVIM_PATH environment variable is unset.") 3 | vim.print( 4 | "Set it to the path of a local copy of https://github.com/echasnovski/mini.nvim/.\n" 5 | ) 6 | os.exit(1) 7 | end 8 | 9 | -- Add plugin to 'runtimepath' to be able to use it in tests 10 | vim.opt.runtimepath:append(vim.fn.getcwd()) 11 | 12 | -- Add 'mini.nvim' to 'runtimepath' to be able to use 'mini.test' 13 | vim.opt.runtimepath:append(vim.env.MINI_NVIM_PATH) 14 | 15 | -- Set up 'mini.test' 16 | local MiniTest = require("mini.test") 17 | MiniTest.setup() 18 | 19 | -- Run test suite 20 | MiniTest.run() 21 | -------------------------------------------------------------------------------- /tests/data/base: -------------------------------------------------------------------------------- 1 | X 2 | X 3 | X 4 | apple 5 | grape 6 | orange 7 | X 8 | X 9 | X 10 | -------------------------------------------------------------------------------- /tests/data/fruits.txt: -------------------------------------------------------------------------------- 1 | X 2 | X 3 | X 4 | <<<<<<< Conflict 1 of 1 5 | %%%%%%% Changes from base to side #1 6 | apple 7 | -grape 8 | +grapefruit 9 | orange 10 | +++++++ Contents of side #2 11 | APPLE 12 | GRAPE 13 | ORANGE 14 | >>>>>>> Conflict 1 of 1 ends 15 | X 16 | X 17 | X 18 | -------------------------------------------------------------------------------- /tests/data/left: -------------------------------------------------------------------------------- 1 | X 2 | X 3 | X 4 | apple 5 | grapefruit 6 | orange 7 | X 8 | X 9 | X 10 | -------------------------------------------------------------------------------- /tests/data/long_markers.txt: -------------------------------------------------------------------------------- 1 | <<<<<<<<<<<<<<< Conflict 1 of 1 2 | %%%%%%%%%%%%%%% Changes from base to side #1 3 | -Heading 4 | +HEADING 5 | ======= 6 | +++++++++++++++ Contents of side #2 7 | New Heading 8 | =========== 9 | >>>>>>>>>>>>>>> Conflict 1 of 1 ends 10 | -------------------------------------------------------------------------------- /tests/data/missing_newline_markers.txt: -------------------------------------------------------------------------------- 1 | <<<<<<< Conflict 1 of 1 2 | +++++++ Contents of side #1 (no terminating newline) 3 | grapefruit 4 | %%%%%%% Changes from base to side #2 (adds terminating newline) 5 | -grape 6 | +grape 7 | >>>>>>> Conflict 1 of 1 ends 8 | -------------------------------------------------------------------------------- /tests/data/multiple_conflicts.txt: -------------------------------------------------------------------------------- 1 | X 2 | X 3 | X 4 | <<<<<<< Conflict 1 of 2 5 | %%%%%%% Changes from base to side #1 6 | apple 7 | -grape 8 | +grapefruit 9 | orange 10 | +++++++ Contents of side #2 11 | APPLE 12 | GRAPE 13 | ORANGE 14 | >>>>>>> Conflict 1 of 2 ends 15 | X 16 | X 17 | X 18 | X 19 | X 20 | <<<<<<< Conflict 2 of 2 21 | %%%%%%% Changes from base to side #1 22 | apple 23 | grape 24 | -orange 25 | +blood orange 26 | +++++++ Contents of side #2 27 | APPLE 28 | GRAPE 29 | ORANGE 30 | >>>>>>> Conflict 2 of 2 ends 31 | -------------------------------------------------------------------------------- /tests/data/right: -------------------------------------------------------------------------------- 1 | X 2 | X 3 | X 4 | APPLE 5 | GRAPE 6 | ORANGE 7 | X 8 | X 9 | X 10 | -------------------------------------------------------------------------------- /tests/screenshots/fruits_ui: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01| X │ X 3 | 02| X │ X 4 | 03| X │ X 5 | 04| apple │ APPLE 6 | 05| grapefruit │ GRAPE 7 | 06| orange │ ORANGE 8 | 07| X │ X 9 | 08| X │ X 10 | 09| X │ X 11 | 10|~ │~ 12 | 11|~ │~ 13 | 12|~ │~ 14 | 13|~ │~ 15 | 14|~ │~ 16 | 15|~ │~ 17 | 16|~ │~ 18 | 17|~ │~ 19 | 18|~ │~ 20 | 19|~ │~ 21 | 20|~ │~ 22 | 21|~ │~ 23 | 22|~ │~ 24 | 23|[No Name] [+] 4,1 All snapshot [RO] 4,1 All 25 | 24|Resolve conflicts leftward then save. Use :cq to abort. 26 | 27 | --|---------|---------|---------|---------|---------|---------|---------|---------| 28 | 01|00111111111111111111111111111111111111120011111111111111111111111111111111111111 29 | 02|00111111111111111111111111111111111111120011111111111111111111111111111111111111 30 | 03|00111111111111111111111111111111111111120011111111111111111111111111111111111111 31 | 04|00333334444444444444444444444444444444420033333444444444444444444444444444444444 32 | 05|00333333333344444444444444444444444444420033333444444444444444444444444444444444 33 | 06|00333333444444444444444444444444444444420033333344444444444444444444444444444444 34 | 07|00111111111111111111111111111111111111120011111111111111111111111111111111111111 35 | 08|00111111111111111111111111111111111111120011111111111111111111111111111111111111 36 | 09|00111111111111111111111111111111111111120011111111111111111111111111111111111111 37 | 10|00000000000000000000000000000000000000020000000000000000000000000000000000000000 38 | 11|00000000000000000000000000000000000000020000000000000000000000000000000000000000 39 | 12|00000000000000000000000000000000000000020000000000000000000000000000000000000000 40 | 13|00000000000000000000000000000000000000020000000000000000000000000000000000000000 41 | 14|00000000000000000000000000000000000000020000000000000000000000000000000000000000 42 | 15|00000000000000000000000000000000000000020000000000000000000000000000000000000000 43 | 16|00000000000000000000000000000000000000020000000000000000000000000000000000000000 44 | 17|00000000000000000000000000000000000000020000000000000000000000000000000000000000 45 | 18|00000000000000000000000000000000000000020000000000000000000000000000000000000000 46 | 19|00000000000000000000000000000000000000020000000000000000000000000000000000000000 47 | 20|00000000000000000000000000000000000000020000000000000000000000000000000000000000 48 | 21|00000000000000000000000000000000000000020000000000000000000000000000000000000000 49 | 22|00000000000000000000000000000000000000020000000000000000000000000000000000000000 50 | 23|55555555555555555555555555555555555555556666666666666666666666666666666666666666 51 | 24|77777777777777777777777777777777777777777777777777777778888888888888888888888888 52 | -------------------------------------------------------------------------------- /tests/screenshots/long_markers_ui: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01| HEADING │ New Heading 3 | 02| ======= │ =========== 4 | 03|~ │~ 5 | 04|~ │~ 6 | 05|~ │~ 7 | 06|~ │~ 8 | 07|~ │~ 9 | 08|~ │~ 10 | 09|~ │~ 11 | 10|~ │~ 12 | 11|~ │~ 13 | 12|~ │~ 14 | 13|~ │~ 15 | 14|~ │~ 16 | 15|~ │~ 17 | 16|~ │~ 18 | 17|~ │~ 19 | 18|~ │~ 20 | 19|~ │~ 21 | 20|~ │~ 22 | 21|~ │~ 23 | 22|~ │~ 24 | 23|[No Name] [+] 1,1 All snapshot [RO] 1,1 All 25 | 24|Resolve conflicts leftward then save. Use :cq to abort. 26 | 27 | --|---------|---------|---------|---------|---------|---------|---------|---------| 28 | 01|00111111122222222222222222222222222222230011111111111222222222222222222222222222 29 | 02|00222222222222222222222222222222222222230022222221111222222222222222222222222222 30 | 03|00000000000000000000000000000000000000030000000000000000000000000000000000000000 31 | 04|00000000000000000000000000000000000000030000000000000000000000000000000000000000 32 | 05|00000000000000000000000000000000000000030000000000000000000000000000000000000000 33 | 06|00000000000000000000000000000000000000030000000000000000000000000000000000000000 34 | 07|00000000000000000000000000000000000000030000000000000000000000000000000000000000 35 | 08|00000000000000000000000000000000000000030000000000000000000000000000000000000000 36 | 09|00000000000000000000000000000000000000030000000000000000000000000000000000000000 37 | 10|00000000000000000000000000000000000000030000000000000000000000000000000000000000 38 | 11|00000000000000000000000000000000000000030000000000000000000000000000000000000000 39 | 12|00000000000000000000000000000000000000030000000000000000000000000000000000000000 40 | 13|00000000000000000000000000000000000000030000000000000000000000000000000000000000 41 | 14|00000000000000000000000000000000000000030000000000000000000000000000000000000000 42 | 15|00000000000000000000000000000000000000030000000000000000000000000000000000000000 43 | 16|00000000000000000000000000000000000000030000000000000000000000000000000000000000 44 | 17|00000000000000000000000000000000000000030000000000000000000000000000000000000000 45 | 18|00000000000000000000000000000000000000030000000000000000000000000000000000000000 46 | 19|00000000000000000000000000000000000000030000000000000000000000000000000000000000 47 | 20|00000000000000000000000000000000000000030000000000000000000000000000000000000000 48 | 21|00000000000000000000000000000000000000030000000000000000000000000000000000000000 49 | 22|00000000000000000000000000000000000000030000000000000000000000000000000000000000 50 | 23|44444444444444444444444444444444444444445555555555555555555555555555555555555555 51 | 24|66666666666666666666666666666666666666666666666666666667777777777777777777777777 52 | -------------------------------------------------------------------------------- /tests/screenshots/missing_newline_ui: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01| grape │ grapefruit 3 | 02|~ │~ 4 | 03|~ │~ 5 | 04|~ │~ 6 | 05|~ │~ 7 | 06|~ │~ 8 | 07|~ │~ 9 | 08|~ │~ 10 | 09|~ │~ 11 | 10|~ │~ 12 | 11|~ │~ 13 | 12|~ │~ 14 | 13|~ │~ 15 | 14|~ │~ 16 | 15|~ │~ 17 | 16|~ │~ 18 | 17|~ │~ 19 | 18|~ │~ 20 | 19|~ │~ 21 | 20|~ │~ 22 | 21|~ │~ 23 | 22|~ │~ 24 | 23|[No Name] [+] 1,1 All snapshot [RO] 1,1 All 25 | 24|Resolve conflicts leftward then save. Use :cq to abort. 26 | 27 | --|---------|---------|---------|---------|---------|---------|---------|---------| 28 | 01|00111111111111111111111111111111111111120011111333331111111111111111111111111111 29 | 02|00000000000000000000000000000000000000020000000000000000000000000000000000000000 30 | 03|00000000000000000000000000000000000000020000000000000000000000000000000000000000 31 | 04|00000000000000000000000000000000000000020000000000000000000000000000000000000000 32 | 05|00000000000000000000000000000000000000020000000000000000000000000000000000000000 33 | 06|00000000000000000000000000000000000000020000000000000000000000000000000000000000 34 | 07|00000000000000000000000000000000000000020000000000000000000000000000000000000000 35 | 08|00000000000000000000000000000000000000020000000000000000000000000000000000000000 36 | 09|00000000000000000000000000000000000000020000000000000000000000000000000000000000 37 | 10|00000000000000000000000000000000000000020000000000000000000000000000000000000000 38 | 11|00000000000000000000000000000000000000020000000000000000000000000000000000000000 39 | 12|00000000000000000000000000000000000000020000000000000000000000000000000000000000 40 | 13|00000000000000000000000000000000000000020000000000000000000000000000000000000000 41 | 14|00000000000000000000000000000000000000020000000000000000000000000000000000000000 42 | 15|00000000000000000000000000000000000000020000000000000000000000000000000000000000 43 | 16|00000000000000000000000000000000000000020000000000000000000000000000000000000000 44 | 17|00000000000000000000000000000000000000020000000000000000000000000000000000000000 45 | 18|00000000000000000000000000000000000000020000000000000000000000000000000000000000 46 | 19|00000000000000000000000000000000000000020000000000000000000000000000000000000000 47 | 20|00000000000000000000000000000000000000020000000000000000000000000000000000000000 48 | 21|00000000000000000000000000000000000000020000000000000000000000000000000000000000 49 | 22|00000000000000000000000000000000000000020000000000000000000000000000000000000000 50 | 23|44444444444444444444444444444444444444445555555555555555555555555555555555555555 51 | 24|66666666666666666666666666666666666666666666666666666667777777777777777777777777 52 | -------------------------------------------------------------------------------- /tests/screenshots/multiple_conflicts_ui: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01| X │ X 3 | 02| X │ X 4 | 03| X │ X 5 | 04| apple │ APPLE 6 | 05| grapefruit │ GRAPE 7 | 06| orange │ ORANGE 8 | 07| X │ X 9 | 08| X │ X 10 | 09| X │ X 11 | 10| X │ X 12 | 11| X │ X 13 | 12| apple │ APPLE 14 | 13| grape │ GRAPE 15 | 14| blood orange │ ORANGE 16 | 15|~ │~ 17 | 16|~ │~ 18 | 17|~ │~ 19 | 18|~ │~ 20 | 19|~ │~ 21 | 20|~ │~ 22 | 21|~ │~ 23 | 22|~ │~ 24 | 23|~ │~ 25 | 24|~ │~ 26 | 25|~ │~ 27 | 26|~ │~ 28 | 27|~ │~ 29 | 28|~ │~ 30 | 29|~ │~ 31 | 30|~ │~ 32 | 31|~ │~ 33 | 32|~ │~ 34 | 33|~ │~ 35 | 34|~ │~ 36 | 35|[No Name] [+] 4,1 All snapshot [RO] 4,1 All 37 | 36|Resolve conflicts leftward then save. Use :cq to abort. 38 | 39 | --|---------|---------|---------|---------|---------|---------|---------|---------| 40 | 01|00111111111111111111111111111111111111120011111111111111111111111111111111111111 41 | 02|00111111111111111111111111111111111111120011111111111111111111111111111111111111 42 | 03|00111111111111111111111111111111111111120011111111111111111111111111111111111111 43 | 04|00333334444444444444444444444444444444420033333444444444444444444444444444444444 44 | 05|00333333333344444444444444444444444444420033333444444444444444444444444444444444 45 | 06|00333333444444444444444444444444444444420033333344444444444444444444444444444444 46 | 07|00111111111111111111111111111111111111120011111111111111111111111111111111111111 47 | 08|00111111111111111111111111111111111111120011111111111111111111111111111111111111 48 | 09|00111111111111111111111111111111111111120011111111111111111111111111111111111111 49 | 10|00111111111111111111111111111111111111120011111111111111111111111111111111111111 50 | 11|00111111111111111111111111111111111111120011111111111111111111111111111111111111 51 | 12|00333334444444444444444444444444444444420033333444444444444444444444444444444444 52 | 13|00333334444444444444444444444444444444420033333444444444444444444444444444444444 53 | 14|00333333333333444444444444444444444444420033333344444444444444444444444444444444 54 | 15|00000000000000000000000000000000000000020000000000000000000000000000000000000000 55 | 16|00000000000000000000000000000000000000020000000000000000000000000000000000000000 56 | 17|00000000000000000000000000000000000000020000000000000000000000000000000000000000 57 | 18|00000000000000000000000000000000000000020000000000000000000000000000000000000000 58 | 19|00000000000000000000000000000000000000020000000000000000000000000000000000000000 59 | 20|00000000000000000000000000000000000000020000000000000000000000000000000000000000 60 | 21|00000000000000000000000000000000000000020000000000000000000000000000000000000000 61 | 22|00000000000000000000000000000000000000020000000000000000000000000000000000000000 62 | 23|00000000000000000000000000000000000000020000000000000000000000000000000000000000 63 | 24|00000000000000000000000000000000000000020000000000000000000000000000000000000000 64 | 25|00000000000000000000000000000000000000020000000000000000000000000000000000000000 65 | 26|00000000000000000000000000000000000000020000000000000000000000000000000000000000 66 | 27|00000000000000000000000000000000000000020000000000000000000000000000000000000000 67 | 28|00000000000000000000000000000000000000020000000000000000000000000000000000000000 68 | 29|00000000000000000000000000000000000000020000000000000000000000000000000000000000 69 | 30|00000000000000000000000000000000000000020000000000000000000000000000000000000000 70 | 31|00000000000000000000000000000000000000020000000000000000000000000000000000000000 71 | 32|00000000000000000000000000000000000000020000000000000000000000000000000000000000 72 | 33|00000000000000000000000000000000000000020000000000000000000000000000000000000000 73 | 34|00000000000000000000000000000000000000020000000000000000000000000000000000000000 74 | 35|55555555555555555555555555555555555555556666666666666666666666666666666666666666 75 | 36|77777777777777777777777777777777777777777777777777777778888888888888888888888888 76 | -------------------------------------------------------------------------------- /tests/screenshots/tests-test_jj_diffconflicts.lua---history-view---displays-UI: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01| 2+ t/d/fruits.txt 3 base X 3 | 02| X │ X │ X 4 | 03| X │ X │ X 5 | 04| X │ X │ X 6 | 05| apple │ apple │ APPLE 7 | 06| grapefruit │ grape │ GRAPE 8 | 07| orange │ orange │ ORANGE 9 | 08| X │ X │ X 10 | 09| X │ X │ X 11 | 10| X │ X │ X 12 | 11|~ │~ │~ 13 | 12|~ │~ │~ 14 | 13|~ │~ │~ 15 | 14|~ │~ │~ 16 | 15|~ │~ │~ 17 | 16|~ │~ │~ 18 | 17|~ │~ │~ 19 | 18|~ │~ │~ 20 | 19|~ │~ │~ 21 | 20|~ │~ │~ 22 | 21|~ │~ │~ 23 | 22|~ │~ │~ 24 | 23|left base right 25 | 24|Resolve conflicts leftward then save. Use :cq to abort. 26 | 27 | --|---------|---------|---------|---------|---------|---------|---------|---------| 28 | 01|01000000000000000002322222200000000000000000000000000000000000000000000000000000 29 | 02|44555555555555555555555555644555555555555555555555555644555555555555555555555555 30 | 03|44555555555555555555555555644555555555555555555555555644555555555555555555555555 31 | 04|44555555555555555555555555644555555555555555555555555644555555555555555555555555 32 | 05|44777778888888888888888888644777778888888888888888888644777778888888888888888888 33 | 06|44777777777788888888888888644777778888888888888888888644777778888888888888888888 34 | 07|44777777888888888888888888644777777888888888888888888644777777888888888888888888 35 | 08|44555555555555555555555555644555555555555555555555555644555555555555555555555555 36 | 09|44555555555555555555555555644555555555555555555555555644555555555555555555555555 37 | 10|44555555555555555555555555644555555555555555555555555644555555555555555555555555 38 | 11|44444444444444444444444444644444444444444444444444444644444444444444444444444444 39 | 12|44444444444444444444444444644444444444444444444444444644444444444444444444444444 40 | 13|44444444444444444444444444644444444444444444444444444644444444444444444444444444 41 | 14|44444444444444444444444444644444444444444444444444444644444444444444444444444444 42 | 15|44444444444444444444444444644444444444444444444444444644444444444444444444444444 43 | 16|44444444444444444444444444644444444444444444444444444644444444444444444444444444 44 | 17|44444444444444444444444444644444444444444444444444444644444444444444444444444444 45 | 18|44444444444444444444444444644444444444444444444444444644444444444444444444444444 46 | 19|44444444444444444444444444644444444444444444444444444644444444444444444444444444 47 | 20|44444444444444444444444444644444444444444444444444444644444444444444444444444444 48 | 21|44444444444444444444444444644444444444444444444444444644444444444444444444444444 49 | 22|44444444444444444444444444644444444444444444444444444644444444444444444444444444 50 | 23|00000000000000000000000000099999999999999999999999999900000000000000000000000000 51 | 24|:::::::::::::::::::::::::::::::::::::::::::::::::::::::;;;;;;;;;;;;;;;;;;;;;;;;; 52 | -------------------------------------------------------------------------------- /tests/screenshots/tests-test_jj_diffconflicts.lua---run---displays-an-error-when-no-valid-conflict: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01|hello world 3 | 02|~ 4 | 03|~ 5 | 04|~ 6 | 05|~ 7 | 06|~ 8 | 07|~ 9 | 08|~ 10 | 09|~ 11 | 10|~ 12 | 11|~ 13 | 12|~ 14 | 13|~ 15 | 14|~ 16 | 15|~ 17 | 16|~ 18 | 17|~ 19 | 18|~ 20 | 19|~ 21 | 20|~ 22 | 21|~ 23 | 22|~ 24 | 23|[No Name] [+] 1,1 All 25 | 24|jj-diffconflicts: no conflicts found in buffer 26 | 27 | --|---------|---------|---------|---------|---------|---------|---------|---------| 28 | 01|00000000000000000000000000000000000000000000000000000000000000000000000000000000 29 | 02|11111111111111111111111111111111111111111111111111111111111111111111111111111111 30 | 03|11111111111111111111111111111111111111111111111111111111111111111111111111111111 31 | 04|11111111111111111111111111111111111111111111111111111111111111111111111111111111 32 | 05|11111111111111111111111111111111111111111111111111111111111111111111111111111111 33 | 06|11111111111111111111111111111111111111111111111111111111111111111111111111111111 34 | 07|11111111111111111111111111111111111111111111111111111111111111111111111111111111 35 | 08|11111111111111111111111111111111111111111111111111111111111111111111111111111111 36 | 09|11111111111111111111111111111111111111111111111111111111111111111111111111111111 37 | 10|11111111111111111111111111111111111111111111111111111111111111111111111111111111 38 | 11|11111111111111111111111111111111111111111111111111111111111111111111111111111111 39 | 12|11111111111111111111111111111111111111111111111111111111111111111111111111111111 40 | 13|11111111111111111111111111111111111111111111111111111111111111111111111111111111 41 | 14|11111111111111111111111111111111111111111111111111111111111111111111111111111111 42 | 15|11111111111111111111111111111111111111111111111111111111111111111111111111111111 43 | 16|11111111111111111111111111111111111111111111111111111111111111111111111111111111 44 | 17|11111111111111111111111111111111111111111111111111111111111111111111111111111111 45 | 18|11111111111111111111111111111111111111111111111111111111111111111111111111111111 46 | 19|11111111111111111111111111111111111111111111111111111111111111111111111111111111 47 | 20|11111111111111111111111111111111111111111111111111111111111111111111111111111111 48 | 21|11111111111111111111111111111111111111111111111111111111111111111111111111111111 49 | 22|11111111111111111111111111111111111111111111111111111111111111111111111111111111 50 | 23|22222222222222222222222222222222222222222222222222222222222222222222222222222222 51 | 24|33333333333333333333333333333333333333333333334444444444444444444444444444444444 52 | -------------------------------------------------------------------------------- /tests/screenshots/tests-test_jj_diffconflicts.lua---run---does-not-work-with-wrong-marker-length: -------------------------------------------------------------------------------- 1 | --|---------|---------|---------|---------|---------|---------|---------|---------| 2 | 01|<<<<<<<<<<<<<<< Conflict 1 of 1 3 | 02|%%%%%%%%%%%%%%% Changes from base to side #1 4 | 03|-Heading 5 | 04|+HEADING 6 | 05| ======= 7 | 06|+++++++++++++++ Contents of side #2 8 | 07|New Heading 9 | 08|=========== 10 | 09|>>>>>>>>>>>>>>> Conflict 1 of 1 ends 11 | 10|~ 12 | 11|~ 13 | 12|~ 14 | 13|~ 15 | 14|~ 16 | 15|~ 17 | 16|~ 18 | 17|~ 19 | 18|~ 20 | 19|~ 21 | 20|~ 22 | 21|~ 23 | 22|~ 24 | 23|[No Name] [+] 1,1 All 25 | 24|jj-diffconflicts: no conflicts found in buffer 26 | 27 | --|---------|---------|---------|---------|---------|---------|---------|---------| 28 | 01|00000000000000000000000000000000000000000000000000000000000000000000000000000000 29 | 02|00000000000000000000000000000000000000000000000000000000000000000000000000000000 30 | 03|00000000000000000000000000000000000000000000000000000000000000000000000000000000 31 | 04|00000000000000000000000000000000000000000000000000000000000000000000000000000000 32 | 05|00000000000000000000000000000000000000000000000000000000000000000000000000000000 33 | 06|00000000000000000000000000000000000000000000000000000000000000000000000000000000 34 | 07|00000000000000000000000000000000000000000000000000000000000000000000000000000000 35 | 08|00000000000000000000000000000000000000000000000000000000000000000000000000000000 36 | 09|00000000000000000000000000000000000000000000000000000000000000000000000000000000 37 | 10|11111111111111111111111111111111111111111111111111111111111111111111111111111111 38 | 11|11111111111111111111111111111111111111111111111111111111111111111111111111111111 39 | 12|11111111111111111111111111111111111111111111111111111111111111111111111111111111 40 | 13|11111111111111111111111111111111111111111111111111111111111111111111111111111111 41 | 14|11111111111111111111111111111111111111111111111111111111111111111111111111111111 42 | 15|11111111111111111111111111111111111111111111111111111111111111111111111111111111 43 | 16|11111111111111111111111111111111111111111111111111111111111111111111111111111111 44 | 17|11111111111111111111111111111111111111111111111111111111111111111111111111111111 45 | 18|11111111111111111111111111111111111111111111111111111111111111111111111111111111 46 | 19|11111111111111111111111111111111111111111111111111111111111111111111111111111111 47 | 20|11111111111111111111111111111111111111111111111111111111111111111111111111111111 48 | 21|11111111111111111111111111111111111111111111111111111111111111111111111111111111 49 | 22|11111111111111111111111111111111111111111111111111111111111111111111111111111111 50 | 23|22222222222222222222222222222222222222222222222222222222222222222222222222222222 51 | 24|33333333333333333333333333333333333333333333334444444444444444444444444444444444 52 | -------------------------------------------------------------------------------- /tests/test_internal.lua: -------------------------------------------------------------------------------- 1 | -- vim: foldmethod=marker 2 | 3 | -- Setup {{{1 4 | local MiniTest = require("mini.test") 5 | local expect, eq = MiniTest.expect, MiniTest.expect.equality 6 | 7 | local jj = require("jj-diffconflicts") 8 | 9 | local read_file = function(filename) return vim.iter(io.lines(filename)):totable() end 10 | 11 | local default_patterns = jj.get_patterns(jj.get_jj_version(), 7) 12 | 13 | local T = MiniTest.new_set() 14 | 15 | -- parse_diff {{{1 16 | T["parse_diff"] = MiniTest.new_set() 17 | T["parse_diff"]["parses valid diff"] = function() 18 | local parsed_diff = jj.parse_diff({ 19 | " apple", 20 | "-grape", 21 | "+grapefruit", 22 | " orange", 23 | }) 24 | eq( 25 | parsed_diff, 26 | { old = { "apple", "grape", "orange" }, new = { "apple", "grapefruit", "orange" } } 27 | ) 28 | end 29 | T["parse_diff"]["raises an error on invalid diff"] = function() 30 | local diff = { 31 | " apple", 32 | "$grape", 33 | "+grapefruit", 34 | " orange", 35 | } 36 | local expected_err = [[unexpected diff line: "$grape"]] 37 | expect.error(function() jj.parse_diff(diff) end, expected_err) 38 | end 39 | 40 | -- parse_conflict {{{1 41 | T["parse_conflict"] = MiniTest.new_set() 42 | T["parse_conflict"]["handles conflict with diff before snaphsot"] = function() 43 | local parsed_conflict = jj.parse_conflict(default_patterns, { 44 | top = 2, 45 | bottom = 12, 46 | lines = { 47 | "%%%%%%% Changes from base to side #1", 48 | " apple", 49 | "-grape", 50 | "+grapefruit", 51 | " orange", 52 | "+++++++ Contents of side #2", 53 | "APPLE", 54 | "GRAPE", 55 | "ORANGE", 56 | }, 57 | }) 58 | eq(parsed_conflict, { 59 | top_line = 2, 60 | bottom_line = 12, 61 | left_side = { "apple", "grapefruit", "orange" }, 62 | right_side = { "APPLE", "GRAPE", "ORANGE" }, 63 | }) 64 | end 65 | T["parse_conflict"]["handles conflict with snaphsot before diff"] = function() 66 | local parsed_conflict = jj.parse_conflict(default_patterns, { 67 | top = 2, 68 | bottom = 12, 69 | lines = { 70 | "+++++++ Contents of side #2", 71 | "APPLE", 72 | "GRAPE", 73 | "ORANGE", 74 | "%%%%%%% Changes from base to side #1", 75 | " apple", 76 | "-grape", 77 | "+grapefruit", 78 | " orange", 79 | }, 80 | }) 81 | eq(parsed_conflict, { 82 | top_line = 2, 83 | bottom_line = 12, 84 | left_side = { "apple", "grapefruit", "orange" }, 85 | right_side = { "APPLE", "GRAPE", "ORANGE" }, 86 | }) 87 | end 88 | T["parse_conflict"]["raises an error on invalid conflict"] = function() 89 | local conflict = { 90 | top = 2, 91 | bottom = 14, 92 | lines = { 93 | "apple", 94 | "grapefruit", 95 | "orange", 96 | "||||||| Base", 97 | "apple", 98 | "grape", 99 | "orange", 100 | "=======", 101 | "APPLE", 102 | "GRAPE", 103 | "ORANGE", 104 | }, 105 | } 106 | local expected_err = "unexpected start for conflict: apple" 107 | expect.error(function() jj.parse_conflict(default_patterns, conflict) end, expected_err) 108 | end 109 | 110 | -- extract_conflicts {{{1 111 | T["extract_conflicts"] = MiniTest.new_set() 112 | T["extract_conflicts"]["handles single conflict"] = function() 113 | local lines = read_file("tests/data/fruits.txt") 114 | local conflict = jj.extract_conflicts(default_patterns, lines) 115 | local expected = { 116 | { 117 | top = 4, 118 | bottom = 14, 119 | lines = { 120 | "%%%%%%% Changes from base to side #1", 121 | " apple", 122 | "-grape", 123 | "+grapefruit", 124 | " orange", 125 | "+++++++ Contents of side #2", 126 | "APPLE", 127 | "GRAPE", 128 | "ORANGE", 129 | }, 130 | }, 131 | } 132 | eq(conflict, expected) 133 | end 134 | T["extract_conflicts"]["handles multiple conflicts"] = function() 135 | local lines = read_file("tests/data/multiple_conflicts.txt") 136 | local conflict = jj.extract_conflicts(default_patterns, lines) 137 | local expected = { 138 | { 139 | top = 4, 140 | bottom = 14, 141 | lines = { 142 | "%%%%%%% Changes from base to side #1", 143 | " apple", 144 | "-grape", 145 | "+grapefruit", 146 | " orange", 147 | "+++++++ Contents of side #2", 148 | "APPLE", 149 | "GRAPE", 150 | "ORANGE", 151 | }, 152 | }, 153 | { 154 | top = 20, 155 | bottom = 30, 156 | lines = { 157 | "%%%%%%% Changes from base to side #1", 158 | " apple", 159 | " grape", 160 | "-orange", 161 | "+blood orange", 162 | "+++++++ Contents of side #2", 163 | "APPLE", 164 | "GRAPE", 165 | "ORANGE", 166 | }, 167 | }, 168 | } 169 | eq(conflict, expected) 170 | end 171 | T["extract_conflicts"]["handles conflicts with missing newline markers"] = function() 172 | local lines = read_file("tests/data/missing_newline_markers.txt") 173 | local conflict = jj.extract_conflicts(default_patterns, lines) 174 | local expected = { 175 | { 176 | top = 1, 177 | bottom = 7, 178 | lines = { 179 | "+++++++ Contents of side #1 (no terminating newline)", 180 | "grapefruit", 181 | "%%%%%%% Changes from base to side #2 (adds terminating newline)", 182 | "-grape", 183 | "+grape", 184 | }, 185 | }, 186 | } 187 | eq(conflict, expected) 188 | end 189 | T["extract_conflicts"]["handles conflict numbered higher than 10"] = function() 190 | local lines = { 191 | "<<<<<<< Conflict 11 of 12", 192 | "%%%%%%% Changes from base to side #1", 193 | " apple", 194 | "-grape", 195 | "+grapefruit", 196 | " orange", 197 | "+++++++ Contents of side #2", 198 | "APPLE", 199 | "GRAPE", 200 | "ORANGE", 201 | ">>>>>>> Conflict 11 of 12 ends", 202 | } 203 | local conflict = jj.extract_conflicts(default_patterns, lines) 204 | local expected = { 205 | { 206 | top = 1, 207 | bottom = 11, 208 | lines = { 209 | "%%%%%%% Changes from base to side #1", 210 | " apple", 211 | "-grape", 212 | "+grapefruit", 213 | " orange", 214 | "+++++++ Contents of side #2", 215 | "APPLE", 216 | "GRAPE", 217 | "ORANGE", 218 | }, 219 | }, 220 | } 221 | eq(conflict, expected) 222 | end 223 | T["extract_conflicts"]["handles invalid conflict with no top"] = function() 224 | local lines = { 225 | "%%%%%%% Changes from base to side #1", 226 | "apple", 227 | "-grape", 228 | "+grapefruit", 229 | "orange", 230 | "+++++++ Contents of side #2", 231 | "APPLE", 232 | "GRAPE", 233 | "ORANGE", 234 | ">>>>>>> Conflict 1 of 1 ends", 235 | } 236 | eq(jj.extract_conflicts(default_patterns, lines), {}) 237 | end 238 | T["extract_conflicts"]["raises an error on invalid conflict with no bottom"] = function() 239 | local lines = { 240 | "<<<<<<< Conflict 1 of 1", 241 | "%%%%%%% Changes from base to side #1", 242 | "apple", 243 | "-grape", 244 | "+grapefruit", 245 | "orange", 246 | "+++++++ Contents of side #2", 247 | "APPLE", 248 | "GRAPE", 249 | "ORANGE", 250 | } 251 | expect.error( 252 | function() jj.extract_conflicts(default_patterns, lines) end, 253 | "could not find bottom marker" 254 | ) 255 | end 256 | T["extract_conflicts"]["raises an error on invalid conflict with no snapshot"] = function() 257 | local lines = { 258 | "<<<<<<< Conflict 1 of 1", 259 | "%%%%%%% Changes from base to side #1", 260 | "apple", 261 | "-grape", 262 | "+grapefruit", 263 | "orange", 264 | ">>>>>>> Conflict 1 of 1 ends", 265 | } 266 | expect.error( 267 | function() jj.extract_conflicts(default_patterns, lines) end, 268 | "could not find snapshot section" 269 | ) 270 | end 271 | 272 | -- validate_conflict {{{1 273 | T["validate_conflict"] = MiniTest.new_set() 274 | T["validate_conflict"]["handles valid conflict"] = function() 275 | local lines = read_file("tests/data/fruits.txt") 276 | expect.no_error(function() jj.validate_conflict(default_patterns, lines) end) 277 | end 278 | T["validate_conflict"]["raises an error on conflict with no diff"] = function() 279 | local lines = { 280 | "<<<<<<< Conflict 1 of 1", 281 | "apple", 282 | "grapefruit", 283 | "orange", 284 | "+++++++ Contents of side #2", 285 | "APPLE", 286 | "GRAPE", 287 | "ORANGE", 288 | ">>>>>>> Conflict 1 of 1 ends", 289 | } 290 | expect.error( 291 | function() jj.validate_conflict(default_patterns, lines) end, 292 | "could not find diff section" 293 | ) 294 | end 295 | T["validate_conflict"]["raises an error on conflict with multiple diffs"] = function() 296 | local lines = { 297 | "<<<<<<< Conflict 1 of 1", 298 | "%%%%%%% Changes from base #1 to side #1", 299 | "apple", 300 | "-grape", 301 | "+grapefruit", 302 | "orange", 303 | "+++++++ Contents of side #2", 304 | "APPLE", 305 | "GRAPE", 306 | "ORANGE", 307 | "%%%%%%% Changes from base #2 to side #3", 308 | "apple", 309 | "-grape", 310 | "+sourgrape", 311 | "orange", 312 | ">>>>>>> Conflict 1 of 1 ends", 313 | } 314 | expect.error( 315 | function() jj.validate_conflict(default_patterns, lines) end, 316 | "conflict has 3 sides" 317 | ) 318 | end 319 | T["validate_conflict"]["raises an error on invalid conflict"] = function() 320 | local lines = { 321 | "<<<<<<< Conflict 1 of 1", 322 | "%%%%%%% Changes from base to side #1", 323 | "apple", 324 | "-grape", 325 | "+grapefruit", 326 | "orange", 327 | ">>>>>>> Conflict 1 of 1 ends", 328 | } 329 | expect.error( 330 | function() jj.validate_conflict(default_patterns, lines) end, 331 | "could not find snapshot" 332 | ) 333 | end 334 | 335 | return T 336 | -------------------------------------------------------------------------------- /tests/test_jj_diffconflicts.lua: -------------------------------------------------------------------------------- 1 | local MiniTest = require("mini.test") 2 | local expect, eq = MiniTest.expect, MiniTest.expect.equality 3 | 4 | local child = MiniTest.new_child_neovim() 5 | 6 | local setup_child = function() 7 | child.lua([[vim.opt.runtimepath:append(vim.fn.getcwd())]]) 8 | child.lua([[jj = require("jj-diffconflicts")]]) 9 | end 10 | local set_lines = function(lines) child.api.nvim_buf_set_lines(0, 0, -1, true, lines) end 11 | local read_file = function(filename) return vim.iter(io.lines(filename)):totable() end 12 | 13 | local SCREENSHOT = { 14 | fruits = "tests/screenshots/fruits_ui", 15 | long_markers = "tests/screenshots/long_markers_ui", 16 | multiple_conflicts = "tests/screenshots/multiple_conflicts_ui", 17 | missing_newline = "tests/screenshots/missing_newline_ui", 18 | } 19 | 20 | local T = MiniTest.new_set({ 21 | hooks = { 22 | post_once = child.stop, 23 | }, 24 | }) 25 | 26 | T["run"] = MiniTest.new_set({ 27 | hooks = { 28 | pre_case = function() 29 | child.restart() 30 | setup_child() 31 | end, 32 | }, 33 | }) 34 | T["run"]["displays UI"] = function() 35 | set_lines(read_file("tests/data/fruits.txt")) 36 | child.lua("jj.run(false, 7)") 37 | expect.reference_screenshot(child.get_screenshot(), SCREENSHOT.fruits) 38 | end 39 | T["run"]["displays an error when no valid conflict"] = function() 40 | set_lines({ "hello world" }) 41 | child.lua("jj.run(false, 7)") 42 | expect.reference_screenshot(child.get_screenshot()) 43 | end 44 | T["run"]["handles conflicts with different marker length"] = function() 45 | set_lines(read_file("tests/data/long_markers.txt")) 46 | child.lua("jj.run(false, 15)") 47 | expect.reference_screenshot(child.get_screenshot(), SCREENSHOT.long_markers) 48 | end 49 | T["run"]["does not work with wrong marker length"] = function() 50 | set_lines(read_file("tests/data/long_markers.txt")) 51 | child.lua("jj.run(false, 7)") 52 | expect.reference_screenshot(child.get_screenshot()) 53 | end 54 | T["run"]["uses g:jj_diffconflicts_marker_length"] = function() 55 | set_lines(read_file("tests/data/long_markers.txt")) 56 | child.g.jj_diffconflicts_marker_length = 15 57 | child.lua("jj.run(false, nil)") 58 | expect.reference_screenshot(child.get_screenshot(), SCREENSHOT.long_markers) 59 | end 60 | T["run"]["defaults to marker length of 7"] = function() 61 | set_lines(read_file("tests/data/fruits.txt")) 62 | eq(child.lua_get("vim.g.jj_diffconflicts_marker_length"), vim.NIL) 63 | child.lua("jj.run(false, nil)") 64 | expect.reference_screenshot(child.get_screenshot(), SCREENSHOT.fruits) 65 | end 66 | T["run"]["raises error for invalid marker length"] = function() 67 | set_lines(read_file("tests/data/fruits.txt")) 68 | expect.error( 69 | function() child.lua("jj.run(false, 'hello')") end, 70 | "marker_length: expected positive number" 71 | ) 72 | end 73 | T["run"]["handles multiple conflicts"] = function() 74 | set_lines(read_file("tests/data/multiple_conflicts.txt")) 75 | child.o.lines = 36 76 | child.lua("jj.run(false, 7)") 77 | expect.reference_screenshot(child.get_screenshot(), SCREENSHOT.multiple_conflicts) 78 | end 79 | T["run"]["handles missing newlines conflicts"] = function() 80 | set_lines(read_file("tests/data/missing_newline_markers.txt")) 81 | child.lua("jj.run(false, 7)") 82 | expect.reference_screenshot(child.get_screenshot(), SCREENSHOT.missing_newline) 83 | end 84 | 85 | T["history view"] = MiniTest.new_set() 86 | T["history view"]["displays UI"] = function() 87 | child.restart({ 88 | "tests/data/fruits.txt", 89 | "tests/data/base", 90 | "tests/data/left", 91 | "tests/data/right", 92 | }) 93 | setup_child() 94 | 95 | child.lua("jj.run(true, 7)") 96 | child.cmd("tabnext") 97 | expect.reference_screenshot(child.get_screenshot()) 98 | end 99 | 100 | return T 101 | --------------------------------------------------------------------------------