refactor: switch git blame to incremental output mode
Also, move blame parser and related structures to a new module VcsBlame. This includes a full parser re-write to take advantage of the slightly more efficient incremental output format.
This commit is contained in:
parent
cb73153fd0
commit
7d1809ba57
6 changed files with 167 additions and 84 deletions
|
|
@ -418,6 +418,11 @@ pub fn build_exe(
|
||||||
.imports = &.{},
|
.imports = &.{},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const VcsBlame_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/VcsBlame.zig"),
|
||||||
|
.imports = &.{},
|
||||||
|
});
|
||||||
|
|
||||||
const color_mod = b.createModule(.{
|
const color_mod = b.createModule(.{
|
||||||
.root_source_file = b.path("src/color.zig"),
|
.root_source_file = b.path("src/color.zig"),
|
||||||
});
|
});
|
||||||
|
|
@ -449,6 +454,7 @@ pub fn build_exe(
|
||||||
.{ .name = "TypedInt", .module = TypedInt_mod },
|
.{ .name = "TypedInt", .module = TypedInt_mod },
|
||||||
.{ .name = "vaxis", .module = vaxis_mod },
|
.{ .name = "vaxis", .module = vaxis_mod },
|
||||||
.{ .name = "file_type_config", .module = file_type_config_mod },
|
.{ .name = "file_type_config", .module = file_type_config_mod },
|
||||||
|
.{ .name = "VcsBlame", .module = VcsBlame_mod },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
146
src/VcsBlame.zig
Normal file
146
src/VcsBlame.zig
Normal file
|
|
@ -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");
|
||||||
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const cbor = @import("cbor");
|
const cbor = @import("cbor");
|
||||||
const TypedInt = @import("TypedInt");
|
const TypedInt = @import("TypedInt");
|
||||||
|
const VcsBlame = @import("VcsBlame");
|
||||||
const file_type_config = @import("file_type_config");
|
const file_type_config = @import("file_type_config");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayList = std.ArrayList;
|
const ArrayList = std.ArrayList;
|
||||||
|
|
@ -55,8 +56,7 @@ meta: ?[]const u8 = null,
|
||||||
lsp_version: usize = 1,
|
lsp_version: usize = 1,
|
||||||
vcs_id: ?[]const u8 = null,
|
vcs_id: ?[]const u8 = null,
|
||||||
vcs_content: ?ArrayList(u8) = null,
|
vcs_content: ?ArrayList(u8) = null,
|
||||||
vcs_blame: ?ArrayList(u8) = null,
|
vcs_blame: VcsBlame = .{},
|
||||||
vcs_blame_items: ArrayList(BlameLine) = .empty,
|
|
||||||
last_view: ?usize = null,
|
last_view: ?usize = null,
|
||||||
|
|
||||||
undo_head: ?*UndoNode = null,
|
undo_head: ?*UndoNode = null,
|
||||||
|
|
@ -1220,7 +1220,7 @@ pub fn create(allocator: Allocator) error{OutOfMemory}!*Self {
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.clear_vcs_content();
|
self.clear_vcs_content();
|
||||||
self.clear_vcs_blame();
|
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.vcs_id) |buf| self.external_allocator.free(buf);
|
||||||
if (self.meta) |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);
|
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 {
|
pub fn clear_vcs_blame(self: *Self) void {
|
||||||
if (self.vcs_blame) |*buf| {
|
self.vcs_blame.reset(self.external_allocator);
|
||||||
buf.deinit(self.external_allocator);
|
|
||||||
self.vcs_blame = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_vcs_blame(self: *const Self) ?[]const u8 {
|
pub fn get_vcs_blame(self: *const Self, line: usize) ?*const VcsBlame.Commit {
|
||||||
return if (self.vcs_blame) |*buf| buf.items else null;
|
return self.vcs_blame.getLine(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_vcs_blame(self: *Self, vcs_blame: []const u8) error{OutOfMemory}!void {
|
pub fn set_vcs_blame(self: *Self, vcs_blame: []const u8) error{OutOfMemory}!void {
|
||||||
if (self.vcs_blame) |*al| {
|
return self.vcs_blame.addContent(self.external_allocator, vcs_blame);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Can probably be optimized
|
pub fn parse_vcs_blame(self: *Self) VcsBlame.Error!void {
|
||||||
pub fn parse_git_blame(self: *Self) !void {
|
return self.vcs_blame.parse(self.external_allocator);
|
||||||
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 update_last_used_time(self: *Self) void {
|
pub fn update_last_used_time(self: *Self) void {
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,7 @@ pub fn blame(context_: usize, file_path: []const u8) !void {
|
||||||
try arg.writer.print("{s}", .{file_path});
|
try arg.writer.print("{s}", .{file_path});
|
||||||
try git(context_, .{
|
try git(context_, .{
|
||||||
"blame",
|
"blame",
|
||||||
"--line-porcelain",
|
"--incremental",
|
||||||
"HEAD",
|
"HEAD",
|
||||||
"--",
|
"--",
|
||||||
arg.written(),
|
arg.written(),
|
||||||
|
|
|
||||||
|
|
@ -1496,7 +1496,8 @@ pub const Editor = struct {
|
||||||
const delta = self.get_delta_lines_until_row(row) orelse return;
|
const delta = self.get_delta_lines_until_row(row) orelse return;
|
||||||
const head_line: i32 = delta + casted_row;
|
const head_line: i32 = delta + casted_row;
|
||||||
if (head_line < 0) return;
|
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.plane.cursor_move_yx(@intCast(row), @intCast(col));
|
||||||
self.render_diagnostic_cell(style);
|
self.render_diagnostic_cell(style);
|
||||||
|
|
@ -1512,9 +1513,9 @@ pub const Editor = struct {
|
||||||
.right => {
|
.right => {
|
||||||
const width = self.plane.window.width;
|
const width = self.plane.window.width;
|
||||||
self.plane.cursor_move_yx(@intCast(row), @intCast(width -| (screen_width - space_begin) + 2));
|
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 {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2160,7 +2160,7 @@ pub fn vcs_blame_update(self: *Self, m: tp.message) void {
|
||||||
buffer.set_vcs_blame(blame_info) catch {};
|
buffer.set_vcs_blame(blame_info) catch {};
|
||||||
} else if (m.match(.{ "PRJ", "git_blame", tp.extract(&file_path), tp.null_ }) catch return) {
|
} 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;
|
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|
|
if (self.get_editor_for_buffer(buffer)) |editor|
|
||||||
editor.vcs_content_update() catch {};
|
editor.vcs_content_update() catch {};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue