diff --git a/build.zig b/build.zig index 88f12a0..4377796 100644 --- a/build.zig +++ b/build.zig @@ -418,6 +418,11 @@ pub fn build_exe( .imports = &.{}, }); + const VcsBlame_mod = b.createModule(.{ + .root_source_file = b.path("src/VcsBlame.zig"), + .imports = &.{}, + }); + const color_mod = b.createModule(.{ .root_source_file = b.path("src/color.zig"), }); @@ -449,6 +454,7 @@ pub fn build_exe( .{ .name = "TypedInt", .module = TypedInt_mod }, .{ .name = "vaxis", .module = vaxis_mod }, .{ .name = "file_type_config", .module = file_type_config_mod }, + .{ .name = "VcsBlame", .module = VcsBlame_mod }, }, }); diff --git a/src/VcsBlame.zig b/src/VcsBlame.zig new file mode 100644 index 0000000..f1cae51 --- /dev/null +++ b/src/VcsBlame.zig @@ -0,0 +1,146 @@ +commits: std.ArrayList(Commit) = .empty, +lines: std.ArrayList(?u16) = .empty, +content: std.ArrayList(u8) = .empty, + +pub const Commit = struct { + // {sha1} {src_line_no} {dst_line_no} {line_count} + id: []const u8 = &.{}, + + // author {text} + author: []const u8 = &.{}, + + // author-mail {email} + @"author-mail": []const u8 = &.{}, + + // author-time {timestamp} + @"author-time": i64 = 0, + + // author-tz {TZ} + @"author-tz": i16 = 0, + + // committer {text} + // committer-mail {email} + // committer-time {timestamp} + // committer-tz {TZ} + + // summary {text} + summary: []const u8 = &.{}, + + // previous {sha1} {filename} + // filename {filename} +}; + +pub fn getLine(self: *const @This(), line: usize) ?*const Commit { + if (line >= self.lines.items.len) return null; + const commit_no = self.lines.items[line] orelse return null; + return &self.commits.items[commit_no]; +} + +pub fn addContent(self: *@This(), allocator: std.mem.Allocator, content: []const u8) error{OutOfMemory}!void { + return self.content.appendSlice(allocator, content); +} + +pub const Error = error{ + OutOfMemory, + InvalidBlameCommitHash, + InvalidBlameCommitSrcLine, + InvalidBlameCommitLine, + InvalidBlameCommitLines, + InvalidBlameHeaderName, + InvalidBlameHeaderValue, + InvalidBlameLineNo, + InvalidBlameLines, + InvalidAuthorTime, + InvalidAuthorTz, +}; + +pub fn parse(self: *@This(), allocator: std.mem.Allocator) Error!void { + self.commits.deinit(allocator); + self.lines.deinit(allocator); + self.commits = .empty; + self.lines = .empty; + + var existing: std.StringHashMapUnmanaged(usize) = .empty; + defer existing.deinit(allocator); + + const headers = enum { author, @"author-mail", @"author-time", @"author-tz", summary, filename }; + + var state: enum { root, commit, headers } = .root; + var commit: Commit = .{}; + var line_no: usize = 0; + var lines: usize = 1; + + var it = std.mem.splitScalar(u8, self.content.items, '\n'); + while (it.next()) |line| { + top: switch (state) { + .root => { + commit = .{}; + line_no = 0; + lines = 0; + state = .commit; + continue :top .commit; + }, + .commit => { // 35be98f95ca999a112ad3aff0932be766f702e13 141 141 1 + var arg = std.mem.splitScalar(u8, line, ' '); + commit.id = arg.next() orelse return error.InvalidBlameCommitHash; + _ = arg.next() orelse return error.InvalidBlameCommitSrcLine; + line_no = std.fmt.parseInt(usize, arg.next() orelse return error.InvalidBlameCommitLine, 10) catch return error.InvalidBlameLineNo; + lines = std.fmt.parseInt(usize, arg.next() orelse return error.InvalidBlameCommitLines, 10) catch return error.InvalidBlameLines; + state = .headers; + }, + .headers => { + var arg = std.mem.splitScalar(u8, line, ' '); + const name = arg.next() orelse return error.InvalidBlameHeaderName; + if (name.len == line.len) return error.InvalidBlameHeaderValue; + const value = line[name.len + 1 ..]; + if (std.meta.stringToEnum(headers, name)) |header| switch (header) { + .author => { + commit.author = value; + }, + .@"author-mail" => { + commit.@"author-mail" = value; + }, + .@"author-time" => { + commit.@"author-time" = std.fmt.parseInt(@TypeOf(commit.@"author-time"), value, 10) catch return error.InvalidAuthorTime; + }, + .@"author-tz" => { + commit.@"author-tz" = std.fmt.parseInt(@TypeOf(commit.@"author-tz"), value, 10) catch return error.InvalidAuthorTz; + }, + .summary => { + commit.summary = value; + }, + .filename => { + line_no -|= 1; + const to_line_no = line_no + lines; + const commit_no: usize = if (existing.get(commit.id)) |n| n else blk: { + const n = self.commits.items.len; + try existing.put(allocator, commit.id, self.commits.items.len); + (try self.commits.addOne(allocator)).* = commit; + break :blk n; + }; + if (self.lines.items.len < to_line_no) { + try self.lines.ensureTotalCapacity(allocator, to_line_no); + while (self.lines.items.len < to_line_no) + (try self.lines.addOne(allocator)).* = null; + } + for (line_no..to_line_no) |ln| + self.lines.items[ln] = @intCast(commit_no); + + state = .root; + }, + }; + }, + } + } +} + +pub fn reset(self: *@This(), allocator: std.mem.Allocator) void { + self.commits.deinit(allocator); + self.lines.deinit(allocator); + self.content.deinit(allocator); + self.commits = .empty; + self.lines = .empty; + self.content = .empty; +} + +const std = @import("std"); diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index bdb2359..e43ce96 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const cbor = @import("cbor"); const TypedInt = @import("TypedInt"); +const VcsBlame = @import("VcsBlame"); const file_type_config = @import("file_type_config"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; @@ -55,8 +56,7 @@ meta: ?[]const u8 = null, lsp_version: usize = 1, vcs_id: ?[]const u8 = null, vcs_content: ?ArrayList(u8) = null, -vcs_blame: ?ArrayList(u8) = null, -vcs_blame_items: ArrayList(BlameLine) = .empty, +vcs_blame: VcsBlame = .{}, last_view: ?usize = null, undo_head: ?*UndoNode = null, @@ -1220,7 +1220,7 @@ pub fn create(allocator: Allocator) error{OutOfMemory}!*Self { pub fn deinit(self: *Self) void { self.clear_vcs_content(); self.clear_vcs_blame(); - self.vcs_blame_items.deinit(self.allocator); + self.vcs_blame.reset(self.external_allocator); if (self.vcs_id) |buf| self.external_allocator.free(buf); if (self.meta) |buf| self.external_allocator.free(buf); if (self.file_buf) |buf| self.external_allocator.free(buf); @@ -1298,89 +1298,19 @@ pub fn get_vcs_content(self: *const Self) ?[]const u8 { } pub fn clear_vcs_blame(self: *Self) void { - if (self.vcs_blame) |*buf| { - buf.deinit(self.external_allocator); - self.vcs_blame = null; - } + self.vcs_blame.reset(self.external_allocator); } -pub fn get_vcs_blame(self: *const Self) ?[]const u8 { - return if (self.vcs_blame) |*buf| buf.items else null; +pub fn get_vcs_blame(self: *const Self, line: usize) ?*const VcsBlame.Commit { + return self.vcs_blame.getLine(line); } pub fn set_vcs_blame(self: *Self, vcs_blame: []const u8) error{OutOfMemory}!void { - if (self.vcs_blame) |*al| { - try al.appendSlice(self.external_allocator, vcs_blame); - } else { - var al: ArrayList(u8) = .empty; - try al.appendSlice(self.external_allocator, vcs_blame); - self.vcs_blame = al; - } + return self.vcs_blame.addContent(self.external_allocator, vcs_blame); } -/// Can probably be optimized -pub fn parse_git_blame(self: *Self) !void { - self.vcs_blame_items.clearAndFree(self.allocator); - var stream = std.io.fixedBufferStream(self.vcs_blame.?.items); - var reader = stream.reader(); - var chunk_active = false; - var current_committer: []u8 = ""; - var current_stamp: usize = undefined; - var buffer: [200]u8 = undefined; - while (true) { - const line = reader.readUntilDelimiter(&buffer, '\n') catch { - break; - }; - - if (line.len == 0) - continue; - - if (line.len >= 40 and std.ascii.isHex(line[0])) { - if (chunk_active) - break; // error - - chunk_active = true; - var it = std.mem.splitScalar(u8, line, ' '); - - const sha = it.next() orelse continue; - if (sha.len != 40) continue; - - _ = it.next(); // original line (ignored) - const number = try std.fmt.parseInt( - i64, - it.next() orelse break, - 10, - ); - if (number != self.vcs_blame_items.items.len + 1) - break; - } else if (std.mem.startsWith(u8, line, "author ")) { - if (!chunk_active) - break; // error - current_committer = try self.allocator.dupe( - u8, - line["author ".len..], - ); - } else if (std.mem.startsWith(u8, line, "author-time ")) { - if (!chunk_active) - break; // error - current_stamp = try std.fmt.parseInt( - usize, - line["author-time ".len..], - 10, - ); - - const ptr = try self.vcs_blame_items.addOne(self.allocator); - ptr.* = BlameLine{ .author_name = current_committer, .author_stamp = current_stamp }; - chunk_active = false; - } - } -} - -pub fn get_line_blame(self: *const Self, line: usize) ?[]const u8 { - if (line + 1 > self.vcs_blame_items.items.len) - return null; - - return self.vcs_blame_items.items[line].author_name; +pub fn parse_vcs_blame(self: *Self) VcsBlame.Error!void { + return self.vcs_blame.parse(self.external_allocator); } pub fn update_last_used_time(self: *Self) void { diff --git a/src/git.zig b/src/git.zig index 60b08c6..79c8a3f 100644 --- a/src/git.zig +++ b/src/git.zig @@ -390,7 +390,7 @@ pub fn blame(context_: usize, file_path: []const u8) !void { try arg.writer.print("{s}", .{file_path}); try git(context_, .{ "blame", - "--line-porcelain", + "--incremental", "HEAD", "--", arg.written(), diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 954656e..e5bec74 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -1496,7 +1496,8 @@ pub const Editor = struct { const delta = self.get_delta_lines_until_row(row) orelse return; const head_line: i32 = delta + casted_row; if (head_line < 0) return; - const blame_name = self.buffer.?.get_line_blame(@intCast(head_line)) orelse return; + const commit = self.buffer.?.get_vcs_blame(@intCast(head_line)) orelse return; + const author = commit.author; self.plane.cursor_move_yx(@intCast(row), @intCast(col)); self.render_diagnostic_cell(style); @@ -1512,9 +1513,9 @@ pub const Editor = struct { .right => { const width = self.plane.window.width; self.plane.cursor_move_yx(@intCast(row), @intCast(width -| (screen_width - space_begin) + 2)); - _ = self.plane.print("  {s}", .{blame_name}) catch 0; + _ = self.plane.print("  {s}", .{author}) catch 0; }, - .left => _ = self.plane.print_aligned_right(@intCast(row), "  {s}", .{blame_name[0..@min(screen_width - space_begin - 3, blame_name.len)]}) catch {}, + .left => _ = self.plane.print_aligned_right(@intCast(row), "  {s}", .{author[0..@min(screen_width - space_begin - 3, author.len)]}) catch {}, } } } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index c1ffade..36869a2 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -2160,7 +2160,7 @@ pub fn vcs_blame_update(self: *Self, m: tp.message) void { buffer.set_vcs_blame(blame_info) catch {}; } else if (m.match(.{ "PRJ", "git_blame", tp.extract(&file_path), tp.null_ }) catch return) { const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return; - buffer.parse_git_blame() catch return; + buffer.parse_vcs_blame() catch return; if (self.get_editor_for_buffer(buffer)) |editor| editor.vcs_content_update() catch {}; }