feat: add git blame with inline display (wip)
This commit is contained in:
parent
3402a54a2e
commit
90a817066a
7 changed files with 223 additions and 2 deletions
|
|
@ -2861,6 +2861,8 @@ pub fn process_git_response(self: *Self, parent: tp.pid_ref, m: tp.message) (Out
|
||||||
var context: usize = undefined;
|
var context: usize = undefined;
|
||||||
var vcs_id: []const u8 = undefined;
|
var vcs_id: []const u8 = undefined;
|
||||||
var vcs_content: []const u8 = undefined;
|
var vcs_content: []const u8 = undefined;
|
||||||
|
var blame_output: []const u8 = undefined;
|
||||||
|
|
||||||
_ = self;
|
_ = self;
|
||||||
|
|
||||||
if (try m.match(.{ tp.any, tp.extract(&context), "rev_parse", tp.extract(&vcs_id) })) {
|
if (try m.match(.{ tp.any, tp.extract(&context), "rev_parse", tp.extract(&vcs_id) })) {
|
||||||
|
|
@ -2876,5 +2878,34 @@ pub fn process_git_response(self: *Self, parent: tp.pid_ref, m: tp.message) (Out
|
||||||
const request: *VcsContentRequest = @ptrFromInt(context);
|
const request: *VcsContentRequest = @ptrFromInt(context);
|
||||||
defer request.deinit();
|
defer request.deinit();
|
||||||
parent.send(.{ "PRJ", "vcs_content", request.file_path, request.vcs_id, null }) catch {};
|
parent.send(.{ "PRJ", "vcs_content", request.file_path, request.vcs_id, null }) catch {};
|
||||||
|
} else if (try m.match(.{ tp.any, tp.extract(&context), "blame", tp.extract(&blame_output) })) {
|
||||||
|
const request: *GitBlameRequest = @ptrFromInt(context);
|
||||||
|
parent.send(.{ "PRJ", "git_blame", request.file_path, blame_output }) catch {};
|
||||||
|
} else if (try m.match(.{ tp.any, tp.extract(&context), "blame", tp.null_ })) {
|
||||||
|
const request: *GitBlameRequest = @ptrFromInt(context);
|
||||||
|
defer request.deinit();
|
||||||
|
parent.send(.{ "PRJ", "git_blame", request.file_path, null }) catch {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_vcs_blame(self: *Self, file_path: []const u8) error{OutOfMemory}!void {
|
||||||
|
const request = try self.allocator.create(GitBlameRequest);
|
||||||
|
request.* = .{
|
||||||
|
.allocator = self.allocator,
|
||||||
|
.project = self,
|
||||||
|
.file_path = try self.allocator.dupe(u8, file_path),
|
||||||
|
};
|
||||||
|
git.blame(@intFromPtr(request), file_path) catch |e|
|
||||||
|
self.logger_git.print_err("blame", "failed: {t}", .{e});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const GitBlameRequest = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
project: *Self,
|
||||||
|
file_path: []const u8,
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This()) void {
|
||||||
|
self.allocator.free(self.file_path);
|
||||||
|
self.allocator.destroy(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,11 @@ pub const Metrics = struct {
|
||||||
pub const egc_last_func = *const fn (self: Metrics, egcs: []const u8) []const u8;
|
pub const egc_last_func = *const fn (self: Metrics, egcs: []const u8) []const u8;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const BlameLine = struct {
|
||||||
|
author_name: []const u8,
|
||||||
|
author_stamp: usize,
|
||||||
|
};
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
external_allocator: Allocator,
|
external_allocator: Allocator,
|
||||||
|
|
@ -50,6 +55,8 @@ 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_items: ArrayList(BlameLine) = .empty,
|
||||||
last_view: ?usize = null,
|
last_view: ?usize = null,
|
||||||
|
|
||||||
undo_head: ?*UndoNode = null,
|
undo_head: ?*UndoNode = null,
|
||||||
|
|
@ -1212,6 +1219,8 @@ 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.vcs_blame_items.deinit(self.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);
|
||||||
|
|
@ -1257,6 +1266,7 @@ pub fn set_vcs_id(self: *Self, vcs_id: []const u8) error{OutOfMemory}!bool {
|
||||||
self.external_allocator.free(old_id);
|
self.external_allocator.free(old_id);
|
||||||
}
|
}
|
||||||
self.clear_vcs_content();
|
self.clear_vcs_content();
|
||||||
|
self.clear_vcs_blame();
|
||||||
self.vcs_id = try self.external_allocator.dupe(u8, vcs_id);
|
self.vcs_id = try self.external_allocator.dupe(u8, vcs_id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1287,6 +1297,92 @@ pub fn get_vcs_content(self: *const Self) ?[]const u8 {
|
||||||
return if (self.vcs_content) |*buf| buf.items else null;
|
return if (self.vcs_content) |*buf| buf.items else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_vcs_blame(self: *Self) void {
|
||||||
|
if (self.vcs_blame) |*buf| {
|
||||||
|
buf.deinit(self.external_allocator);
|
||||||
|
self.vcs_blame = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vcs_blame(self: *const Self) ?[]const u8 {
|
||||||
|
return if (self.vcs_blame) |*buf| buf.items else null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 update_last_used_time(self: *Self) void {
|
pub fn update_last_used_time(self: *Self) void {
|
||||||
self.utime = std.time.milliTimestamp();
|
self.utime = std.time.milliTimestamp();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
src/git.zig
18
src/git.zig
|
|
@ -383,4 +383,22 @@ fn get_git() ?[]const u8 {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn blame(context_: usize, file_path: []const u8) !void {
|
||||||
|
const tag = @src().fn_name;
|
||||||
|
var arg: std.Io.Writer.Allocating = .init(allocator);
|
||||||
|
defer arg.deinit();
|
||||||
|
try arg.writer.print("{s}", .{file_path});
|
||||||
|
try git(context_, .{
|
||||||
|
"blame",
|
||||||
|
"--line-porcelain",
|
||||||
|
"HEAD",
|
||||||
|
"--",
|
||||||
|
arg.written(),
|
||||||
|
}, struct {
|
||||||
|
fn result(context: usize, parent: tp.pid_ref, output: []const u8) void {
|
||||||
|
parent.send(.{ module_name, context, tag, output }) catch {};
|
||||||
|
}
|
||||||
|
}.result, exit_null(tag));
|
||||||
|
}
|
||||||
|
|
||||||
const module_name = @typeName(@This());
|
const module_name = @typeName(@This());
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,13 @@ pub fn request_vcs_content(file_path: []const u8, vcs_id: []const u8) (ProjectMa
|
||||||
return send(.{ "request_vcs_content", project, file_path, vcs_id });
|
return send(.{ "request_vcs_content", project, file_path, vcs_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_vcs_blame(file_path: []const u8) (ProjectManagerError || ProjectError)!void {
|
||||||
|
const project = tp.env.get().str("project");
|
||||||
|
if (project.len == 0)
|
||||||
|
return error.NoProject;
|
||||||
|
return send(.{ "request_vcs_blame", project, file_path });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_task(task: []const u8) (ProjectManagerError || ProjectError)!void {
|
pub fn add_task(task: []const u8) (ProjectManagerError || ProjectError)!void {
|
||||||
const project = tp.env.get().str("project");
|
const project = tp.env.get().str("project");
|
||||||
if (project.len == 0)
|
if (project.len == 0)
|
||||||
|
|
@ -428,6 +435,9 @@ const Process = struct {
|
||||||
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), "cat_file", tp.more })) {
|
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), "cat_file", tp.more })) {
|
||||||
const request: *Project.VcsContentRequest = @ptrFromInt(context);
|
const request: *Project.VcsContentRequest = @ptrFromInt(context);
|
||||||
request.project.process_git_response(self.parent.ref(), m) catch |e| self.logger.err("git-cat-file", e);
|
request.project.process_git_response(self.parent.ref(), m) catch |e| self.logger.err("git-cat-file", e);
|
||||||
|
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), "blame", tp.more })) {
|
||||||
|
const request: *Project.GitBlameRequest = @ptrFromInt(context);
|
||||||
|
request.project.process_git_response(self.parent.ref(), m) catch |e| self.logger.err("git-blame", e);
|
||||||
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), tp.more })) {
|
} else if (try cbor.match(m.buf, .{ "git", tp.extract(&context), tp.more })) {
|
||||||
const project: *Project = @ptrFromInt(context);
|
const project: *Project = @ptrFromInt(context);
|
||||||
project.process_git(self.parent.ref(), m) catch {};
|
project.process_git(self.parent.ref(), m) catch {};
|
||||||
|
|
@ -523,6 +533,8 @@ const Process = struct {
|
||||||
return;
|
return;
|
||||||
} else if (try cbor.match(m.buf, .{ "exit", "error.LspFailed", tp.more })) {
|
} else if (try cbor.match(m.buf, .{ "exit", "error.LspFailed", tp.more })) {
|
||||||
return;
|
return;
|
||||||
|
} else if (try cbor.match(m.buf, .{ "request_vcs_blame", tp.extract(&project_directory), tp.extract(&path) })) {
|
||||||
|
self.request_vcs_blame(from, project_directory, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
|
||||||
} else {
|
} else {
|
||||||
self.logger.err("receive", tp.unexpected(m));
|
self.logger.err("receive", tp.unexpected(m));
|
||||||
}
|
}
|
||||||
|
|
@ -677,6 +689,11 @@ const Process = struct {
|
||||||
try project.request_vcs_content(file_path, vcs_id);
|
try project.request_vcs_content(file_path, vcs_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn request_vcs_blame(self: *Process, _: tp.pid_ref, project_directory: []const u8, file_path: []const u8) (ProjectError || Project.RequestError)!void {
|
||||||
|
const project = self.projects.get(project_directory) orelse return error.NoProject;
|
||||||
|
try project.request_vcs_blame(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
fn did_open(self: *Process, project_directory: []const u8, file_path: []const u8, file_type: []const u8, language_server: []const u8, language_server_options: []const u8, version: usize, text: []const u8) (ProjectError || Project.StartLspError || CallError || cbor.Error)!void {
|
fn did_open(self: *Process, project_directory: []const u8, file_path: []const u8, file_type: []const u8, language_server: []const u8, language_server_options: []const u8, version: usize, text: []const u8) (ProjectError || Project.StartLspError || CallError || cbor.Error)!void {
|
||||||
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_open" });
|
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".did_open" });
|
||||||
defer frame.deinit();
|
defer frame.deinit();
|
||||||
|
|
|
||||||
|
|
@ -1258,6 +1258,7 @@ pub const Editor = struct {
|
||||||
self.render_whitespace_map(theme, ctx_.cell_map) catch {};
|
self.render_whitespace_map(theme, ctx_.cell_map) catch {};
|
||||||
if (tui.config().inline_diagnostics)
|
if (tui.config().inline_diagnostics)
|
||||||
self.render_diagnostics(theme, hl_row, ctx_.cell_map) catch {};
|
self.render_diagnostics(theme, hl_row, ctx_.cell_map) catch {};
|
||||||
|
self.render_blame(theme, ctx_.cell_map) catch {};
|
||||||
self.render_column_highlights() catch {};
|
self.render_column_highlights() catch {};
|
||||||
self.render_cursors(theme, ctx_.cell_map, focused) catch {};
|
self.render_cursors(theme, ctx_.cell_map, focused) catch {};
|
||||||
}
|
}
|
||||||
|
|
@ -1455,6 +1456,44 @@ pub const Editor = struct {
|
||||||
_ = self.plane.putc(&cell) catch {};
|
_ = self.plane.putc(&cell) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_blame(self: *Self, theme: *const Widget.Theme, cell_map: CellMap) !void {
|
||||||
|
const cursor = self.get_primary().cursor;
|
||||||
|
const row_min = self.view.row;
|
||||||
|
const row_max = row_min + self.view.rows;
|
||||||
|
if (cursor.row < row_min or row_max < cursor.row)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const row = cursor.row - self.view.row;
|
||||||
|
const col = cursor.col;
|
||||||
|
|
||||||
|
const screen_width = self.view.cols;
|
||||||
|
var style = theme.editor_hint;
|
||||||
|
style = .{ .fg = style.fg, .bg = theme.editor_hint.bg };
|
||||||
|
|
||||||
|
// @TODO: Get diff rows and edit this line number
|
||||||
|
const blame_name = self.buffer.?.get_line_blame(row) orelse return;
|
||||||
|
|
||||||
|
self.plane.cursor_move_yx(@intCast(row), @intCast(col));
|
||||||
|
self.render_diagnostic_cell(style);
|
||||||
|
|
||||||
|
var space_begin = screen_width;
|
||||||
|
while (space_begin > 0) : (space_begin -= 1)
|
||||||
|
if (cell_map.get_yx(row, space_begin).cell_type != .empty) break;
|
||||||
|
if (screen_width > min_diagnostic_view_len and space_begin < screen_width - min_diagnostic_view_len) {
|
||||||
|
self.plane.set_style(style);
|
||||||
|
|
||||||
|
// Opposite as diagnostics
|
||||||
|
switch (tui.config().inline_diagnostics_alignment) {
|
||||||
|
.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;
|
||||||
|
},
|
||||||
|
.left => _ = self.plane.print_aligned_right(@intCast(row), " {s}", .{blame_name[0..@min(screen_width - space_begin - 3, blame_name.len)]}) catch {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline fn render_selection_cell(_: *const Self, theme: *const Widget.Theme, cell: *Cell) void {
|
inline fn render_selection_cell(_: *const Self, theme: *const Widget.Theme, cell: *Cell) void {
|
||||||
cell.set_style_bg_opaque(theme.editor);
|
cell.set_style_bg_opaque(theme.editor);
|
||||||
cell.set_style_bg(theme.editor_selection);
|
cell.set_style_bg(theme.editor_selection);
|
||||||
|
|
|
||||||
|
|
@ -2129,9 +2129,11 @@ pub fn vcs_id_update(self: *Self, m: tp.message) void {
|
||||||
|
|
||||||
if (m.match(.{ "PRJ", "vcs_id", tp.extract(&file_path), tp.extract(&vcs_id) }) catch return) {
|
if (m.match(.{ "PRJ", "vcs_id", tp.extract(&file_path), tp.extract(&vcs_id) }) 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;
|
||||||
const need_vcs_content = buffer.set_vcs_id(vcs_id) catch false;
|
const vcs_id_updated = buffer.set_vcs_id(vcs_id) catch false;
|
||||||
if (need_vcs_content)
|
if (vcs_id_updated) {
|
||||||
project_manager.request_vcs_content(file_path, vcs_id) catch {};
|
project_manager.request_vcs_content(file_path, vcs_id) catch {};
|
||||||
|
project_manager.request_vcs_blame(file_path) catch {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2150,6 +2152,20 @@ pub fn vcs_content_update(self: *Self, m: tp.message) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn vcs_blame_update(self: *Self, m: tp.message) void {
|
||||||
|
var file_path: []const u8 = undefined;
|
||||||
|
var blame_info: []const u8 = undefined;
|
||||||
|
if (m.match(.{ "PRJ", "git_blame", tp.extract(&file_path), tp.extract(&blame_info) }) catch return) {
|
||||||
|
const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return;
|
||||||
|
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;
|
||||||
|
if (self.get_editor_for_buffer(buffer)) |editor|
|
||||||
|
editor.vcs_content_update() catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn trigger_characters_update(self: *Self, m: tp.message) void {
|
pub fn trigger_characters_update(self: *Self, m: tp.message) void {
|
||||||
self.lsp_info.add_from_event(m.buf) catch return;
|
self.lsp_info.add_from_event(m.buf) catch return;
|
||||||
self.foreach_editor(ed.Editor.update_completion_triggers);
|
self.foreach_editor(ed.Editor.update_completion_triggers);
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,10 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
|
||||||
if (try m.match(.{ "PRJ", "vcs_content", tp.more }))
|
if (try m.match(.{ "PRJ", "vcs_content", tp.more }))
|
||||||
return if (mainview()) |mv| mv.vcs_content_update(m);
|
return if (mainview()) |mv| mv.vcs_content_update(m);
|
||||||
|
|
||||||
|
if (try m.match(.{ "PRJ", "git_blame", tp.more })) {
|
||||||
|
return if (mainview()) |mv| mv.vcs_blame_update(m);
|
||||||
|
}
|
||||||
|
|
||||||
if (try m.match(.{ "PRJ", "triggerCharacters", tp.more }))
|
if (try m.match(.{ "PRJ", "triggerCharacters", tp.more }))
|
||||||
return if (mainview()) |mv| mv.trigger_characters_update(m);
|
return if (mainview()) |mv| mv.trigger_characters_update(m);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue