├── gitdiff.lua ├── init.lua ├── license ├── readme.md ├── screenshot.png ├── test.lua └── test_diff.txt /gitdiff.lua: -------------------------------------------------------------------------------- 1 | -- mod-version:1 2 | local gitdiff = {} 3 | 4 | -- liquidev is a genius 5 | local function extract_hunks(input) 6 | local hunks = {} 7 | local current_hunk = {} 8 | 9 | local function end_hunk(new_line) 10 | if #current_hunk > 0 then 11 | table.insert(hunks, current_hunk) 12 | current_hunk = { new_line } 13 | end 14 | end 15 | 16 | for line in input:gmatch("(.-)\n") do 17 | if line:match("^@") then 18 | end_hunk(line) 19 | else 20 | table.insert(current_hunk, line) 21 | end 22 | end 23 | 24 | -- add the last hunk to the table 25 | end_hunk("") 26 | 27 | return hunks 28 | end 29 | 30 | -- determines if line is a hunk header and returns 31 | -- the starting line of changed version (second set of numbers part before 32 | -- optional comma) or nil 33 | ---@param line string 34 | ---@return integer | nil 35 | local function get_hunk_start(line) 36 | if "string" ~= type(line) or not line:match("^@@[^@]+@@") then 37 | return nil 38 | end 39 | 40 | -- doing "@@%s+-%d+(,%d+)?%s++(%d-)(,%d+)?%s+@@" did not work 41 | -- so we loop through a couple of patterns 42 | local patterns = { 43 | "@@%s+-%d+,%d+%s++(%d-),%d+%s+@@", 44 | "@@%s+-%d+%s++(%d-),%d+%s+@@", 45 | "@@%s+-%d+,%d+%s++(%d-)%s+@@", 46 | "@@%s+-%d+%s++(%d-)%s+@@" 47 | } 48 | 49 | local start 50 | for _, p in ipairs(patterns) do 51 | start = line:match(p) 52 | if start then return tonumber(start) end 53 | end 54 | return nil 55 | end 56 | 57 | -- this will only work on single-file diffs 58 | function gitdiff.changed_lines(diff) 59 | if not diff then return {} end 60 | local changed_lines = {} 61 | local hunks = extract_hunks(diff) 62 | -- iterate over hunks 63 | for _, hunk in ipairs(hunks) do 64 | local current_line 65 | local hunk_start = get_hunk_start(hunk[1]) 66 | if not hunk_start then -- mod 67 | goto continue 68 | end 69 | 70 | current_line = hunk_start - 1 71 | 72 | -- remove hunk header 73 | hunk[1] = "" 74 | 75 | for _, line in ipairs(hunk) do 76 | if line:match("^%s-%[%-.-%-]$") then 77 | table.insert(changed_lines, { 78 | line_number = current_line, 79 | change_type = "deletion" 80 | }) 81 | -- do not add to the current line 82 | goto skip_line 83 | end 84 | 85 | if line:match("^%s-{%+.-+}$") then 86 | table.insert(changed_lines, { 87 | line_number = current_line, 88 | change_type = "addition" 89 | }) 90 | 91 | elseif line:match("{%+.-+}") or line:match("%[%-.-%-]") then 92 | table.insert(changed_lines, { 93 | line_number = current_line, 94 | change_type = "modification" 95 | }) 96 | end 97 | 98 | current_line = current_line + 1 99 | ::skip_line:: 100 | end 101 | ::continue:: 102 | end 103 | 104 | local indexed_changed_lines = {} 105 | for _, line in ipairs(changed_lines) do 106 | indexed_changed_lines[line.line_number] = line.change_type 107 | end 108 | 109 | return indexed_changed_lines 110 | end 111 | 112 | return gitdiff 113 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- mod-version:3 2 | -- Highlights changed lines, if file is in a git repository. 3 | -- Also supports MiniMap, if user has it installed and activated. 4 | local core = require "core" 5 | local config = require "core.config" 6 | local DocView = require "core.docview" 7 | local Doc = require "core.doc" 8 | local common = require "core.common" 9 | local command = require "core.command" 10 | local style = require "core.style" 11 | local gitdiff = require "plugins.gitdiff_highlight.gitdiff" 12 | 13 | -- vscode defaults 14 | style.gitdiff_addition = style.gitdiff_addition or { common.color "#587c0c" } 15 | style.gitdiff_modification = style.gitdiff_modification or { common.color "#0c7d9d" } 16 | style.gitdiff_deletion = style.gitdiff_deletion or { common.color "#94151b" } 17 | 18 | local function color_for_diff(diff) 19 | if diff == "addition" then 20 | return style.gitdiff_addition 21 | elseif diff == "modification" then 22 | return style.gitdiff_modification 23 | else 24 | return style.gitdiff_deletion 25 | end 26 | end 27 | 28 | style.gitdiff_width = style.gitdiff_width or 3 29 | 30 | -- maximum size of git diff to read, multiplied by current filesize 31 | config.plugins.gitdiff_highlight.max_diff_size = 2 32 | 33 | 34 | local diffs = setmetatable({}, { __mode = "k" }) 35 | 36 | local function get_diff(doc) 37 | return diffs[doc] or { is_in_repo = false } 38 | end 39 | 40 | local function gitdiff_padding(dv) 41 | return style.padding.x * 1.5 + dv:get_font():get_width(#dv.doc.lines) 42 | end 43 | 44 | local function update_diff(doc) 45 | if not doc or not doc.abs_filename then return end 46 | 47 | local full_path = doc.abs_filename 48 | core.log_quiet("[gitdiff_highlight] updating diff for " .. full_path) 49 | 50 | local path = full_path:match("(.*" .. PATHSEP .. ")") 51 | 52 | if not get_diff(doc).is_in_repo then 53 | local git_proc = process.start({ 54 | "git", "-C", path, "ls-files", "--error-unmatch", full_path 55 | }) 56 | while git_proc:running() do 57 | coroutine.yield(0.1) 58 | end 59 | if 0 ~= git_proc:returncode() then 60 | core.log_quiet("[gitdiff_highlight] file " 61 | .. full_path .. " is not in a git repository") 62 | 63 | return 64 | end 65 | end 66 | 67 | local max_diff_size 68 | local finfo = system.get_file_info(full_path) 69 | max_diff_size = config.plugins.gitdiff_highlight.max_diff_size * finfo.size 70 | local diff_proc = process.start({ 71 | "git", "-C", path, "diff", "HEAD", "--word-diff", 72 | "--unified=1", "--no-color", full_path 73 | }) 74 | while diff_proc:running() do 75 | coroutine.yield(0.1) 76 | end 77 | diffs[doc] = gitdiff.changed_lines(diff_proc:read_stdout(max_diff_size)) 78 | diffs[doc].is_in_repo = true 79 | end 80 | 81 | local old_docview_gutter = DocView.draw_line_gutter 82 | local old_gutter_width = DocView.get_gutter_width 83 | function DocView:draw_line_gutter(line, x, y, width) 84 | if not get_diff(self.doc).is_in_repo then 85 | return old_docview_gutter(self, line, x, y, width) 86 | end 87 | 88 | local lh = self:get_line_height() 89 | 90 | local gw, gpad = old_gutter_width(self) 91 | 92 | old_docview_gutter(self, line, x, y, gpad and gw - gpad or gw) 93 | 94 | if diffs[self.doc][line] == nil then 95 | return lh 96 | end 97 | 98 | local color = color_for_diff(diffs[self.doc][line]) 99 | 100 | -- add margin in between highlight and text 101 | x = x + gitdiff_padding(self) 102 | 103 | if diffs[self.doc][line] ~= "deletion" then 104 | renderer.draw_rect(x, y, style.gitdiff_width, 105 | lh, color) 106 | return lh 107 | end 108 | 109 | renderer.draw_rect(x - style.gitdiff_width * 2, 110 | y, style.gitdiff_width * 4, 2, color) 111 | 112 | return lh 113 | end 114 | 115 | function DocView:get_gutter_width() 116 | local gw, gpad = old_gutter_width(self) 117 | if not get_diff(self.doc).is_in_repo then return gw, gpad end 118 | 119 | return gw + style.padding.x * style.gitdiff_width / 12, gpad 120 | end 121 | 122 | local old_text_change = Doc.on_text_change 123 | local function on_text_change(doc) 124 | doc.gitdiff_highlight_last_doc_lines = #doc.lines 125 | return old_text_change(doc, type) 126 | end 127 | function Doc:on_text_change(type) 128 | if not get_diff(self).is_in_repo then return on_text_change(self) end 129 | 130 | local line = self:get_selection() 131 | if diffs[self][line] == "addition" then return on_text_change(self) end 132 | 133 | -- TODO figure out how to detect an addition 134 | local last_doc_lines = self.gitdiff_highlight_last_doc_lines or 0 135 | if type == "insert" or (type == "remove" and #self.lines == last_doc_lines) then 136 | diffs[self][line] = "modification" 137 | elseif type == "remove" then 138 | diffs[self][line] = "deletion" 139 | end 140 | return on_text_change(self) 141 | end 142 | 143 | local old_doc_save = Doc.save 144 | function Doc:save(...) 145 | old_doc_save(self, ...) 146 | core.add_thread(update_diff, nil, self) 147 | end 148 | 149 | local old_doc_load = Doc.load 150 | function Doc:load(...) 151 | old_doc_load(self, ...) 152 | self.gitdiff_highlight_last_doc_lines = #self.lines 153 | core.add_thread(update_diff, nil, self) 154 | end 155 | 156 | -- add minimap support only after all plugins are loaded 157 | core.add_thread(function() 158 | -- don't load minimap if user has disabled it 159 | if false == config.plugins.minimap then return end 160 | 161 | -- abort if MiniMap isn't installed 162 | local found, MiniMap = pcall(require, "plugins.minimap") 163 | if not found then return end 164 | 165 | 166 | -- Override MiniMap's line_highlight_color, but first 167 | -- stash the old one 168 | local old_line_highlight_color = MiniMap.line_highlight_color 169 | function MiniMap:line_highlight_color(line_index) 170 | local diff = get_diff(core.active_view.doc) 171 | if diff.is_in_repo and diff[line_index] then 172 | return color_for_diff(diff[line_index]) 173 | end 174 | return old_line_highlight_color(line_index) 175 | end 176 | end) 177 | 178 | local function jump_to_next_change() 179 | local doc = core.active_view.doc 180 | local line, col = doc:get_selection() 181 | if not get_diff(doc).is_in_repo then return end 182 | 183 | while diffs[doc][line] do 184 | line = line + 1 185 | end 186 | 187 | while line <= #doc.lines do 188 | if diffs[doc][line] then 189 | doc:set_selection(line, col, line, col) 190 | return 191 | end 192 | line = line + 1 193 | end 194 | end 195 | 196 | local function jump_to_previous_change() 197 | local doc = core.active_view.doc 198 | local line, col = doc:get_selection() 199 | if not get_diff(doc).is_in_repo then return end 200 | 201 | while diffs[doc][line] do 202 | line = line - 1 203 | end 204 | 205 | while line > 0 do 206 | if diffs[doc][line] then 207 | doc:set_selection(line, col, line, col) 208 | return 209 | end 210 | line = line - 1 211 | end 212 | end 213 | 214 | command.add("core.docview", { 215 | ["gitdiff:previous-change"] = jump_to_previous_change, 216 | ["gitdiff:next-change"] = jump_to_next_change 217 | }) 218 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright 2021 cukmekerb 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # lite-xl git diff highlight plugin 2 | 3 | ![screenshot](screenshot.png) 4 | 5 | This plugin will highlight changed lines of any file tracked by git 6 | 7 | Clone this repo and symlink `init.lua` and `gitdiff.lua` into `USERDIR/plugins/gitdiff_highlight` or download the files and copy them there 8 | 9 | ## gitdiff.lua - an oddly specific git diff parser 10 | 11 | `gitdiff.lua` is a gitdiff parser, so if you want to parse git diffs in lua for any other reason, feel free to use it under the MIT license 12 | 13 | ### usage 14 | 15 | place `gitdiff.lua` in the same folder as the script you'll be using it with 16 | ```lua 17 | local gitdiff = require "gitdiff" 18 | 19 | -- diff is the output of the `git diff` command 20 | -- this only works on single-file diffs 21 | gitdiff.changed_lines(diff) 22 | -- returns a table that looks like the following: 23 | { 24 | nil, 25 | nil, 26 | "addition", 27 | "addition", 28 | nil, 29 | nil, 30 | "modification", 31 | "deletion", 32 | nil, 33 | nil 34 | } 35 | -- where the index of the table is the line number the change corresponds to 36 | ``` 37 | 38 | ### test.lua 39 | pipe a git diff into its stdin and it will run some tests on `gitdiff.lua` 40 | 41 | eg: 42 | `cat test_diff.txt | lua test.lua` 43 | 44 | eg #2: 45 | `git diff gitdiff.lua | lua test.lua` 46 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincens2005/lite-xl-gitdiff-highlight/f0e02b6a7299acbeb4a5f137b26830a6cca96cc8/screenshot.png -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | -- this file tests the script 2 | local gitdiff = require "gitdiff" 3 | 4 | local input = io.read("*a") 5 | local lines = gitdiff.changed_lines(input) -- test 6 | 7 | print("testing first line...") 8 | print(lines[1]) 9 | print() 10 | print("testing all lines...") 11 | for i, line in pairs(lines) do 12 | print("line ".. i .. " - " .. line) 13 | end 14 | -------------------------------------------------------------------------------- /test_diff.txt: -------------------------------------------------------------------------------- 1 | diff --git a/src/main.c b/src/main.c 2 | index 8193d20..f71e3bc 100644 3 | --- a/src/main.c 4 | +++ b/src/main.c 5 | @@ -19,33 +19,34 @@ 6 | 7 | int game_looping = 1; 8 | 9 | int direction = LEFT; {+// modified line+} 10 | 11 | int highscore = 0; 12 | 13 | int game[GAME_RES][GAME_RES]; // 0 is black, 1 is snake, 2 is apple 14 | 15 | [-int snake[1000][3]; // is it there? x, y-] 16 | 17 | [-int snake_len = 15;-] 18 | [-int apple_pos[2];-] 19 | 20 | int score = 0; 21 | 22 | int rand_range(int min, int max) { [-srandom(time(0));-]{+// modified block+} 23 | {+ urmom(time(0));+} 24 | return [-(random()-]{+(urmom()+} % (max - min + 1)) + min; {+// haha+} 25 | } 26 | 27 | void draw_frame() { 28 | gfx_SetDrawBuffer(); 29 | gfx_FillScreen(0); 30 | 31 | {+/*+} 32 | {+ * Added block+} 33 | {+ * there's new stuff in here+} 34 | {+ */+} 35 | 36 | char text_score[13]; // max seven digit number score 37 | [-sprintf(text_score,-]{+sprint(text_score,+} "score %d", score); 38 | fontlib_ClearWindow(); 39 | fontlib_SetCursorPosition(5, fontlib_GetWindowYMin()); 40 | fontlib_DrawString(text_score); {+// single modification+} 41 | 42 | if (highscore > 0) { 43 | char text_hs[16]; 44 | @@ -231,7 +232,7 @@ int main(void) { 45 | //game_looping++; 46 | } 47 | 48 | gfx_End(); {+// change in other hunk+} 49 | 50 | return 0; 51 | } 52 | --------------------------------------------------------------------------------