1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
//! File panes: everything a Pane does BECAUSE it has `file: ?File` set — the
//! disk read, the content swap undo/redo commits through, the tree-sitter
//! highlight window, and the two render passes only a file has (the line
//! number gutter and the syntax recolor). The rest of a file pane's behaviour
//! is the pane machinery in pardes.zig, which does not care what kind it is.
const std = @import("std");
const pardes = @import("pardes.zig");
const config = @import("config.zig");
const Pardes = pardes.Pardes;
const Pane = pardes.Pane;
const File = pardes.File;
const FileSnap = pardes.FileSnap;
const modal = @import("modal.zig");
const look = @import("look.zig");
const syntax = @import("syntax.zig");
const tracy = @import("tracy.zig");
const SYNTAX_CONTEXT_BEFORE_ROWS: usize = 0;
const SYNTAX_CONTEXT_AFTER_ROWS: usize = 2;
pub fn lineCount(content: []const u8) usize {
return std.mem.count(u8, content, "\n") + 1;
}
pub fn open(p: *Pardes, id: usize, path: []const u8, line: usize) !*Pane {
const content = try look.readFile(p.gpa, path);
errdefer p.gpa.free(content);
const path_copy = try p.gpa.dupe(u8, path);
errdefer p.gpa.free(path_copy);
const pane = try p.newDocPane(id);
const total = lineCount(content);
const scroll: usize = if (line > 0 and line <= total) line - 1 else 0;
pane.file = .{ .path = path_copy, .content = content, .scroll = scroll };
pane.cur_pinned = true;
pane.cur_row = @intCast(scroll);
// watches follow pane lifetime: this is the only place a real file is read
// off disk, and deinitPane is the only place one goes away
p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true } });
return pane;
}
pub fn setContent(p: *Pardes, f: *File, new: []u8) void {
p.gpa.free(f.content);
f.content = new;
f.syntax_dirty = true;
}
/// undo/redo restores the selection recorded with the snapshot (helix keeps
/// selections in its history transactions), clamped: the content it was taken
/// against may be shorter than the one it is being restored onto.
pub fn restoreSnap(pane: *Pane, f: *File, snap: FileSnap) void {
const n = modal.lineCount(f.content);
const row: usize = @min(@as(usize, @intCast(@max(0, snap.cur_row))), if (n == 0) 0 else n - 1);
const llen = modal.lineSlice(f.content, row).len;
pane.cur_row = @intCast(row);
pane.cur_col = @intCast(@min(@as(usize, @intCast(@max(0, snap.cur_col))), llen));
pane.vsel = snap.vsel;
pane.msel.active = false;
pane.cur_pinned = true;
pane.sticky_col = -1;
pane.ensureCursorVisible();
}
/// re-highlight the visible window of any file whose syntax went stale
/// (edit, scroll, load) — visible-range-first so big files stay snappy
pub fn refreshHighlights(p: *Pardes) void {
const tz = tracy.zone(@src(), "refreshHighlights");
defer tz.end();
for (p.panes) |slot| {
const pane = slot orelse continue;
if (pane.file == null) continue;
const f = &pane.file.?;
if (!f.syntax_dirty) continue;
if (!p.colors_on) {
if (f.highlights.len > 0) p.gpa.free(f.highlights);
f.highlights = &.{};
f.highlight_start = 0;
f.syntax_dirty = false;
continue;
}
const start_row = f.scroll -| SYNTAX_CONTEXT_BEFORE_ROWS;
const start = modal.lineStartOffset(f.content, start_row);
const end_row = start_row + SYNTAX_CONTEXT_BEFORE_ROWS + pane.rows + SYNTAX_CONTEXT_AFTER_ROWS;
const end = @max(start, modal.lineStartOffset(f.content, end_row));
const new_highlights = syntax.highlightFileRange(p.gpa, f.path, f.content, start, end) catch {
f.syntax_dirty = false;
continue;
};
if (f.highlights.len > 0) p.gpa.free(f.highlights);
f.highlights = new_highlights;
f.highlight_start = if (f.highlights.len > 0) start else 0;
f.syntax_dirty = false;
}
}
/// the body a file pane renders: `pane.rows` content lines from the scroll
/// offset, each behind its right-aligned line number, then cut by hscroll.
pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File) ![]const u8 {
var out: std.ArrayList(u8) = .empty;
var flines = std.mem.splitScalar(u8, f.content, '\n');
var skipped: usize = 0;
while (skipped < f.scroll) : (skipped += 1) {
if (flines.next() == null) break;
}
var i: usize = 0;
while (i < pane.rows) : (i += 1) {
if (i > 0) try out.append(arena, '\n');
const abs: i32 = @as(i32, @intCast(f.scroll)) + @as(i32, @intCast(i));
const row_start = out.items.len;
if (flines.next()) |text| {
var lbuf: [16]u8 = undefined;
// unsigned: {d} prints a leading '+' for signed ints
const lineno: usize = @intCast(abs + 1);
const prefix = std.fmt.bufPrint(&lbuf, "{d: >4} ", .{lineno}) catch " ";
var base: std.ArrayList(u8) = .empty;
try base.appendSlice(arena, prefix);
try base.appendSlice(arena, text);
try out.appendSlice(arena, base.items);
}
// horizontal scroll: cut hscroll columns after the gutter
if (pane.hscroll > 0) {
const cut_at = row_start + config.PREFIX_W;
if (out.items.len > cut_at) {
const cut = @min(@as(usize, @intCast(pane.hscroll)), out.items.len - cut_at);
out.replaceRangeAssumeCapacity(cut_at, cut, &.{});
}
}
}
return out.items;
}
/// line-number gutter: mute the first PREFIX_W columns. Cheap chrome, not
/// gated on colors_on; selection/cursor passes still win. The cursor row's
/// number takes the tag style (same row math as renderPane's cursor pass) so
/// the eye finds the current line.
pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16, active: bool) void {
const s = &p.surface;
const th = p.theme();
const goff = pane.scroll();
const gcur = pane.vt.screens.active.cursor;
const gcrow = if (pane.cur_pinned) pane.cur_row else @as(i32, @intCast(gcur.y)) + goff;
const cur_vr: i32 = if (active and !pane.tag_edit) gcrow - goff else -1;
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
const on_cursor = @as(i32, vr) == cur_vr;
var c: u16 = 0;
while (c < config.PREFIX_W and c < tw) : (c += 1) {
const cell = s.at(tx + c, r.y + pardes.BOX_H + vr);
cell.default = false; // paints blank gutter rows too
if (on_cursor) {
cell.style.fg = .{ .rgb = th.tag_fg };
cell.style.bg = .{ .rgb = th.tag_bg };
} else cell.style.fg = .{ .rgb = th.lineno };
}
}
}
const SynStyle = struct { fg: [3]u8, bold: bool };
fn synStyle(p: *Pardes, sy: syntax.Syn) ?SynStyle {
return switch (sy) {
.none => null,
.keyword => .{ .fg = p.theme().kw, .bold = true },
.string => .{ .fg = p.theme().str, .bold = false },
.number => .{ .fg = p.theme().num, .bold = false },
.comment => .{ .fg = p.theme().comment, .bold = true },
};
}
/// syntax colors: recolor each content cell from its tree-sitter style byte;
/// content starts after the lineno gutter
pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16, tw: u16, body_h: u16) void {
if (f.highlights.len == 0) return;
const s = &p.surface;
const tz_recolor = tracy.zone(@src(), "synRecolor");
defer tz_recolor.end();
var flines = std.mem.splitScalar(u8, f.content, '\n');
var skip: usize = 0;
while (skip < f.scroll) : (skip += 1) {
if (flines.next() == null) break;
}
const hs: usize = @intCast(@max(0, pane.hscroll));
var vr: u16 = 0;
while (vr < body_h) : (vr += 1) {
const line = flines.next() orelse break;
const base = @intFromPtr(line.ptr) - @intFromPtr(f.content.ptr);
var c: usize = 0;
while (hs + c < line.len and config.PREFIX_W + c < tw) : (c += 1) {
const idx = base + hs + c;
if (idx < f.highlight_start) continue;
const hidx = idx - f.highlight_start;
if (hidx >= f.highlights.len) continue;
const ss = synStyle(p, @enumFromInt(f.highlights[hidx])) orelse continue;
const cell = s.at(tx + @as(u16, @intCast(config.PREFIX_W + c)), r.y + pardes.BOX_H + vr);
if (cell.default) continue;
cell.style.fg = .{ .rgb = ss.fg };
cell.style.bold = ss.bold;
}
}
}
|