seendiff 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/bin/seendiff.js +9 -0
- package/package.json +38 -0
- package/src/cli.js +216 -0
- package/src/git.js +615 -0
- package/src/highlight.js +220 -0
- package/src/server.js +580 -0
- package/src/store.js +128 -0
- package/src/theme-base.css +482 -0
- package/src/theme.js +14 -0
- package/src/walkthrough.js +338 -0
- package/static/fonts/JetBrainsMono.woff2 +0 -0
- package/static/fonts/LICENCE-UbuntuSansMono.txt +96 -0
- package/static/fonts/LICENSE-JetBrainsMono.txt +93 -0
- package/static/fonts/README.md +29 -0
- package/static/fonts/UbuntuSansMono.woff2 +0 -0
- package/static/index.html +3175 -0
|
@@ -0,0 +1,3175 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>seendiff</title>
|
|
7
|
+
<link rel="icon"
|
|
8
|
+
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16' shape-rendering='crispEdges'><defs><filter id='g' x='-50%' y='-50%' width='200%' height='200%' color-interpolation-filters='linearRGB'><feGaussianBlur stdDeviation='0.9' result='b'/><feMerge><feMergeNode in='b'/><feMergeNode in='b'/><feMergeNode in='b'/></feMerge></filter><clipPath id='t'><rect width='16' height='16' rx='3'/></clipPath></defs><rect width='16' height='16' rx='3' fill='%23111113'/><g clip-path='url(%23t)'><g fill='%23a3be8c' filter='url(%23g)'><rect x='6' y='3' width='1' height='1'/><rect x='9' y='3' width='1' height='1'/><rect x='5' y='4' width='1' height='1'/><rect x='10' y='4' width='1' height='1'/><rect x='4' y='5' width='1' height='1'/><rect x='11' y='5' width='1' height='1'/><rect x='3' y='6' width='1' height='1'/><rect x='12' y='6' width='1' height='1'/><rect x='2' y='7' width='1' height='1'/><rect x='13' y='7' width='1' height='1'/><rect x='3' y='8' width='1' height='1'/><rect x='12' y='8' width='1' height='1'/><rect x='4' y='9' width='1' height='1'/><rect x='11' y='9' width='1' height='1'/><rect x='5' y='10' width='1' height='1'/><rect x='10' y='10' width='1' height='1'/><rect x='6' y='11' width='1' height='1'/><rect x='9' y='11' width='1' height='1'/></g><g fill='%23a3be8c'><rect x='6' y='3' width='1' height='1'/><rect x='9' y='3' width='1' height='1'/><rect x='5' y='4' width='1' height='1'/><rect x='10' y='4' width='1' height='1'/><rect x='4' y='5' width='1' height='1'/><rect x='11' y='5' width='1' height='1'/><rect x='3' y='6' width='1' height='1'/><rect x='12' y='6' width='1' height='1'/><rect x='2' y='7' width='1' height='1'/><rect x='13' y='7' width='1' height='1'/><rect x='3' y='8' width='1' height='1'/><rect x='12' y='8' width='1' height='1'/><rect x='4' y='9' width='1' height='1'/><rect x='11' y='9' width='1' height='1'/><rect x='5' y='10' width='1' height='1'/><rect x='10' y='10' width='1' height='1'/><rect x='6' y='11' width='1' height='1'/><rect x='9' y='11' width='1' height='1'/></g></g></svg>">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<script>
|
|
11
|
+
(function () {
|
|
12
|
+
var prefs = __PREFS__;
|
|
13
|
+
for (var k in prefs) localStorage.setItem(k, prefs[k]);
|
|
14
|
+
})();
|
|
15
|
+
(function () {
|
|
16
|
+
var t = localStorage.getItem("seendiff.theme");
|
|
17
|
+
if (t === "solarized") t = "solarized-dark";
|
|
18
|
+
if (!t) t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
19
|
+
document.documentElement.dataset.theme = t;
|
|
20
|
+
var f = localStorage.getItem("seendiff.font") || "JetBrains Mono";
|
|
21
|
+
document.documentElement.style.setProperty("--mono-font", "'" + f + "'");
|
|
22
|
+
})();
|
|
23
|
+
</script>
|
|
24
|
+
<link rel="stylesheet" href="/theme.css">
|
|
25
|
+
<style>
|
|
26
|
+
@font-face {
|
|
27
|
+
font-family: 'JetBrains Mono';
|
|
28
|
+
font-weight: 400 700;
|
|
29
|
+
src: url('/fonts/JetBrainsMono.woff2') format('woff2');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@font-face {
|
|
33
|
+
font-family: 'Ubuntu Sans Mono';
|
|
34
|
+
font-weight: 400 700;
|
|
35
|
+
src: url('/fonts/UbuntuSansMono.woff2') format('woff2');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:root {
|
|
39
|
+
--mono-font: 'JetBrains Mono';
|
|
40
|
+
--line-height: 20px;
|
|
41
|
+
--font-size: 12.5px;
|
|
42
|
+
--neon: color-mix(in srgb, var(--add-sign) 82%, #fff);
|
|
43
|
+
--file-neon: var(--neon);
|
|
44
|
+
--trough: color-mix(in srgb, #000 58%, var(--bg));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:root[data-theme="light"] {
|
|
48
|
+
--bg: #ffffff;
|
|
49
|
+
--bg2: #f2f2f3;
|
|
50
|
+
--fg: #1d1d1f;
|
|
51
|
+
--fg-dim: #6e6e73;
|
|
52
|
+
--border: #d6d6da;
|
|
53
|
+
--accent: #5f6b7a;
|
|
54
|
+
--add-bg: rgba(26, 127, 55, .08);
|
|
55
|
+
--del-bg: rgba(207, 34, 46, .08);
|
|
56
|
+
--add-sign: #1a7f37;
|
|
57
|
+
--del-sign: #cf222e;
|
|
58
|
+
--mixed: #9a6700;
|
|
59
|
+
--sel-bg: rgba(95, 107, 122, .16);
|
|
60
|
+
--focus-ring: #5f6b7a;
|
|
61
|
+
--toast-bg: #3a3a3f;
|
|
62
|
+
--toast-fg: #ffffff;
|
|
63
|
+
--neon: var(--add-sign);
|
|
64
|
+
--trough: color-mix(in srgb, #000 12%, var(--bg));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:root[data-theme="dark"] {
|
|
68
|
+
--bg: #0d1117;
|
|
69
|
+
--bg2: #161b22;
|
|
70
|
+
--fg: #c9d1d9;
|
|
71
|
+
--fg-dim: #8b949e;
|
|
72
|
+
--border: #30363d;
|
|
73
|
+
--accent: #58a6ff;
|
|
74
|
+
--add-bg: rgba(63, 185, 80, .09);
|
|
75
|
+
--del-bg: rgba(248, 81, 73, .09);
|
|
76
|
+
--add-sign: #3fb950;
|
|
77
|
+
--del-sign: #f85149;
|
|
78
|
+
--mixed: #d29922;
|
|
79
|
+
--sel-bg: rgba(56, 139, 253, .25);
|
|
80
|
+
--focus-ring: #58a6ff;
|
|
81
|
+
--toast-bg: #c9d1d9;
|
|
82
|
+
--toast-fg: #0d1117;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:root[data-theme="solarized-dark"] {
|
|
86
|
+
--bg: #002b36;
|
|
87
|
+
--bg2: #073642;
|
|
88
|
+
--fg: #93a1a1;
|
|
89
|
+
--fg-dim: #657b83;
|
|
90
|
+
--border: #0a4552;
|
|
91
|
+
--accent: #268bd2;
|
|
92
|
+
--add-bg: rgba(133, 153, 0, .10);
|
|
93
|
+
--del-bg: rgba(220, 50, 47, .10);
|
|
94
|
+
--add-sign: #859900;
|
|
95
|
+
--del-sign: #dc322f;
|
|
96
|
+
--mixed: #b58900;
|
|
97
|
+
--sel-bg: rgba(38, 139, 210, .25);
|
|
98
|
+
--focus-ring: #268bd2;
|
|
99
|
+
--toast-bg: #eee8d5;
|
|
100
|
+
--toast-fg: #002b36;
|
|
101
|
+
--trough: color-mix(in srgb, #000 18%, var(--bg));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:root[data-theme="solarized-light"] {
|
|
105
|
+
--bg: #fdf6e3;
|
|
106
|
+
--bg2: #eee8d5;
|
|
107
|
+
--fg: #586e75;
|
|
108
|
+
--fg-dim: #93a1a1;
|
|
109
|
+
--border: #ded8c4;
|
|
110
|
+
--accent: #268bd2;
|
|
111
|
+
--add-bg: rgba(133, 153, 0, .14);
|
|
112
|
+
--del-bg: rgba(220, 50, 47, .12);
|
|
113
|
+
--add-sign: #859900;
|
|
114
|
+
--del-sign: #dc322f;
|
|
115
|
+
--mixed: #b58900;
|
|
116
|
+
--sel-bg: rgba(38, 139, 210, .18);
|
|
117
|
+
--focus-ring: #268bd2;
|
|
118
|
+
--toast-bg: #073642;
|
|
119
|
+
--toast-fg: #eee8d5;
|
|
120
|
+
--neon: var(--add-sign);
|
|
121
|
+
--trough: color-mix(in srgb, #000 12%, var(--bg));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:root[data-theme="space"] {
|
|
125
|
+
--bg: #111113;
|
|
126
|
+
--bg2: #1a1a1d;
|
|
127
|
+
--fg: #b8bbc2;
|
|
128
|
+
--fg-dim: #77797f;
|
|
129
|
+
--border: #2b2b30;
|
|
130
|
+
--accent: #9aa4ae;
|
|
131
|
+
--add-bg: rgba(163, 190, 140, .09);
|
|
132
|
+
--del-bg: rgba(191, 97, 106, .10);
|
|
133
|
+
--add-sign: #a3be8c;
|
|
134
|
+
--del-sign: #bf616a;
|
|
135
|
+
--mixed: #ebcb8b;
|
|
136
|
+
--sel-bg: rgba(154, 164, 174, .20);
|
|
137
|
+
--focus-ring: #9aa4ae;
|
|
138
|
+
--toast-bg: #d8d8dc;
|
|
139
|
+
--toast-fg: #111113;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
:root[data-theme="hailmary"] {
|
|
143
|
+
--bg: #0c1210;
|
|
144
|
+
--bg2: #161e1a;
|
|
145
|
+
--fg: #c6c0b4;
|
|
146
|
+
--fg-dim: #7e786d;
|
|
147
|
+
--border: #2c322d;
|
|
148
|
+
--accent: #4e8f94;
|
|
149
|
+
--add-bg: rgba(143, 156, 94, .10);
|
|
150
|
+
--del-bg: rgba(184, 94, 58, .10);
|
|
151
|
+
--add-sign: #8f9c5e;
|
|
152
|
+
--del-sign: #b85e3a;
|
|
153
|
+
--mixed: #c19a51;
|
|
154
|
+
--sel-bg: rgba(78, 143, 148, .22);
|
|
155
|
+
--focus-ring: #4e8f94;
|
|
156
|
+
--toast-bg: #d2ccc0;
|
|
157
|
+
--toast-fg: #0c1210;
|
|
158
|
+
--neon: #ff4a1f;
|
|
159
|
+
--file-neon: #ffa53d;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
:root[data-theme="hailmary"] .hl .na {
|
|
163
|
+
color: #c19a51;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
:root[data-theme="hailmary"] .hl .k,
|
|
167
|
+
:root[data-theme="hailmary"] .hl .kc,
|
|
168
|
+
:root[data-theme="hailmary"] .hl .kd,
|
|
169
|
+
:root[data-theme="hailmary"] .hl .kn,
|
|
170
|
+
:root[data-theme="hailmary"] .hl .kr,
|
|
171
|
+
:root[data-theme="hailmary"] .hl .kt {
|
|
172
|
+
color: #b85e3a;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:root[data-theme="hailmary"] .hl .nf,
|
|
176
|
+
:root[data-theme="hailmary"] .hl .nd {
|
|
177
|
+
color: #d9a892;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
:root[data-theme="hailmary"] .hl .c,
|
|
181
|
+
:root[data-theme="hailmary"] .hl .c1,
|
|
182
|
+
:root[data-theme="hailmary"] .hl .cm,
|
|
183
|
+
:root[data-theme="hailmary"] .hl .cs {
|
|
184
|
+
color: #6f6a60;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
* {
|
|
188
|
+
box-sizing: border-box;
|
|
189
|
+
margin: 0;
|
|
190
|
+
padding: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
* {
|
|
194
|
+
scrollbar-color: var(--sb-thumb) transparent;
|
|
195
|
+
scrollbar-width: thin;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:root {
|
|
199
|
+
--sb-thumb: color-mix(in srgb, var(--fg) 26%, transparent);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
::-webkit-scrollbar {
|
|
203
|
+
width: 10px;
|
|
204
|
+
height: 10px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
::-webkit-scrollbar-track {
|
|
208
|
+
background: transparent;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
::-webkit-scrollbar-thumb {
|
|
212
|
+
background: var(--sb-thumb);
|
|
213
|
+
border-radius: 5px;
|
|
214
|
+
border: 2px solid transparent;
|
|
215
|
+
background-clip: content-box;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
::-webkit-scrollbar-thumb:hover {
|
|
219
|
+
background: color-mix(in srgb, var(--accent) 65%, var(--fg));
|
|
220
|
+
background-clip: content-box;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
::-webkit-scrollbar-corner {
|
|
224
|
+
background: transparent;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
html,
|
|
228
|
+
body {
|
|
229
|
+
height: 100%;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
body {
|
|
233
|
+
background: var(--bg);
|
|
234
|
+
color: var(--fg);
|
|
235
|
+
font: 13px/1.45 system-ui, sans-serif;
|
|
236
|
+
display: flex;
|
|
237
|
+
overflow: hidden;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
button {
|
|
241
|
+
font: inherit;
|
|
242
|
+
color: inherit;
|
|
243
|
+
background: none;
|
|
244
|
+
border: 1px solid var(--border);
|
|
245
|
+
border-radius: 4px;
|
|
246
|
+
padding: 2px 8px;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
button:hover {
|
|
251
|
+
border-color: var(--accent);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#sidebar {
|
|
255
|
+
width: 300px;
|
|
256
|
+
min-width: 160px;
|
|
257
|
+
max-width: 60vw;
|
|
258
|
+
background: var(--bg2);
|
|
259
|
+
border-right: 1px solid var(--border);
|
|
260
|
+
display: flex;
|
|
261
|
+
flex-direction: column;
|
|
262
|
+
flex: none;
|
|
263
|
+
position: relative;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#sb-resizer {
|
|
267
|
+
position: absolute;
|
|
268
|
+
top: 0;
|
|
269
|
+
right: -3px;
|
|
270
|
+
width: 6px;
|
|
271
|
+
height: 100%;
|
|
272
|
+
cursor: col-resize;
|
|
273
|
+
z-index: 5;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#sb-resizer:hover,
|
|
277
|
+
#sb-resizer.active {
|
|
278
|
+
background: var(--accent);
|
|
279
|
+
opacity: .5;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#sb-head {
|
|
283
|
+
padding: 10px 12px 8px;
|
|
284
|
+
border-bottom: 1px solid var(--border);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#sb-title {
|
|
288
|
+
font-weight: 600;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#sb-title .wm-seen {
|
|
292
|
+
color: color-mix(in srgb, var(--fg) 35%, #fff);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#sb-title .wm-diff {
|
|
296
|
+
color: color-mix(in srgb, var(--fg) 70%, var(--fg-dim));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
:root[data-theme="light"] #sb-title .wm-seen,
|
|
300
|
+
:root[data-theme="solarized-light"] #sb-title .wm-seen {
|
|
301
|
+
color: var(--fg);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#sb-stats {
|
|
305
|
+
color: var(--fg-dim);
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
margin-top: 2px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#sb-base {
|
|
311
|
+
color: var(--fg-dim);
|
|
312
|
+
font-size: 11px;
|
|
313
|
+
margin-top: 2px;
|
|
314
|
+
font-family: var(--mono-font), monospace;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#dirty-banner {
|
|
318
|
+
margin-top: 6px;
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
color: var(--mixed);
|
|
321
|
+
cursor: default;
|
|
322
|
+
display: none;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#sb-progress,
|
|
326
|
+
#sb-progress span {
|
|
327
|
+
--notches: repeating-linear-gradient(90deg, var(--notch) 0 var(--notch-w),
|
|
328
|
+
transparent var(--notch-w) var(--notch-p));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#sb-progress {
|
|
332
|
+
--notch: color-mix(in srgb, var(--fg) 20%, transparent);
|
|
333
|
+
--notch-w: 7px;
|
|
334
|
+
--notch-p: 10px;
|
|
335
|
+
position: relative;
|
|
336
|
+
height: 15px;
|
|
337
|
+
margin-top: 8px;
|
|
338
|
+
width: var(--pbar-w, 100%);
|
|
339
|
+
padding: 3px var(--pbar-pad, 3px);
|
|
340
|
+
border-radius: 3px;
|
|
341
|
+
background-image: var(--notches), linear-gradient(var(--trough), var(--trough));
|
|
342
|
+
background-origin: content-box, border-box;
|
|
343
|
+
background-clip: content-box, border-box;
|
|
344
|
+
background-repeat: repeat, no-repeat;
|
|
345
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, #000 35%, transparent),
|
|
346
|
+
inset 0 1px 3px color-mix(in srgb, #000 45%, transparent);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#sb-progress span {
|
|
350
|
+
position: absolute;
|
|
351
|
+
left: var(--pbar-pad, 3px);
|
|
352
|
+
top: 3px;
|
|
353
|
+
bottom: 3px;
|
|
354
|
+
background-image: var(--notches);
|
|
355
|
+
transition: width .3s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#sb-progress .seen {
|
|
359
|
+
--notch: color-mix(in srgb, var(--neon) 72%, transparent);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#sb-progress .rev {
|
|
363
|
+
--notch: var(--neon);
|
|
364
|
+
z-index: 1;
|
|
365
|
+
transition: width .3s, filter .4s;
|
|
366
|
+
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--neon) 70%, transparent)) drop-shadow(0 0 calc(2px * var(--glow, 0)) color-mix(in srgb, var(--neon) 70%, transparent)) drop-shadow(0 0 calc(5px * var(--glow, 0)) color-mix(in srgb, var(--neon) 45%, transparent)) drop-shadow(0 0 calc(9px * var(--glow, 0)) color-mix(in srgb, var(--neon) 30%, transparent));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
:is(:root[data-theme="light"], :root[data-theme="dark"],
|
|
370
|
+
:root[data-theme="solarized-dark"], :root[data-theme="solarized-light"]):is(#sb-progress, #sb-progress span) {
|
|
371
|
+
--notches: linear-gradient(var(--notch), var(--notch));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
:is(:root[data-theme="light"], :root[data-theme="dark"],
|
|
375
|
+
:root[data-theme="solarized-dark"], :root[data-theme="solarized-light"]) #sb-progress span {
|
|
376
|
+
border-radius: 2px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
:is(:root[data-theme="light"], :root[data-theme="solarized-light"]):is(#sb-progress, .f-bar) .rev {
|
|
380
|
+
filter: none;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
:root[data-theme="space"] #sb-progress .rev {
|
|
384
|
+
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--neon) 70%, transparent)) drop-shadow(0 0 calc(4px * var(--glow, 0)) color-mix(in srgb, var(--neon) 70%, transparent)) drop-shadow(0 0 calc(9px * var(--glow, 0)) color-mix(in srgb, var(--neon) 45%, transparent)) drop-shadow(0 0 calc(18px * var(--glow, 0)) color-mix(in srgb, var(--neon) 30%, transparent));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
:root[data-theme="hailmary"] #sb-progress .rev {
|
|
388
|
+
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--neon) 70%, transparent)) drop-shadow(0 0 calc(2px * var(--glow, 0)) #fff6) drop-shadow(0 0 calc(6px * var(--glow, 0)) color-mix(in srgb, var(--neon) 70%, transparent)) drop-shadow(0 0 calc(13px * var(--glow, 0)) color-mix(in srgb, var(--neon) 45%, transparent)) drop-shadow(0 0 calc(24px * var(--glow, 0)) color-mix(in srgb, var(--neon) 30%, transparent));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#confetti {
|
|
392
|
+
position: fixed;
|
|
393
|
+
inset: 0;
|
|
394
|
+
pointer-events: none;
|
|
395
|
+
z-index: 30;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#filter {
|
|
399
|
+
margin: 8px 10px;
|
|
400
|
+
padding: 4px 8px;
|
|
401
|
+
width: calc(100% - 20px);
|
|
402
|
+
background: var(--bg);
|
|
403
|
+
color: var(--fg);
|
|
404
|
+
border: 1px solid var(--border);
|
|
405
|
+
border-radius: 4px;
|
|
406
|
+
font: inherit;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#filter:focus {
|
|
410
|
+
outline: 1px solid var(--accent);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#filelist {
|
|
414
|
+
overflow-y: auto;
|
|
415
|
+
flex: 1;
|
|
416
|
+
padding-bottom: 12px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.f-item,
|
|
420
|
+
.d-item {
|
|
421
|
+
padding: 4px 10px 4px 8px;
|
|
422
|
+
cursor: pointer;
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: 6px;
|
|
426
|
+
border-left: 2px solid transparent;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.d-item {
|
|
430
|
+
color: var(--fg-dim);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.d-item:hover {
|
|
434
|
+
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.d-arrow {
|
|
438
|
+
width: 12px;
|
|
439
|
+
flex: none;
|
|
440
|
+
text-align: center;
|
|
441
|
+
font-size: 10px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.d-name {
|
|
445
|
+
flex: 1;
|
|
446
|
+
min-width: 0;
|
|
447
|
+
overflow: hidden;
|
|
448
|
+
text-overflow: ellipsis;
|
|
449
|
+
white-space: nowrap;
|
|
450
|
+
font-family: var(--mono-font), monospace;
|
|
451
|
+
font-size: 12px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.f-item:hover {
|
|
455
|
+
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.f-item.cur {
|
|
459
|
+
border-left-color: var(--accent);
|
|
460
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.f-glyph {
|
|
464
|
+
width: 14px;
|
|
465
|
+
text-align: center;
|
|
466
|
+
flex: none;
|
|
467
|
+
cursor: pointer;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.f-glyph.done {
|
|
471
|
+
color: var(--add-sign);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.f-name {
|
|
475
|
+
flex: 1;
|
|
476
|
+
min-width: 0;
|
|
477
|
+
overflow: hidden;
|
|
478
|
+
text-overflow: ellipsis;
|
|
479
|
+
white-space: nowrap;
|
|
480
|
+
direction: rtl;
|
|
481
|
+
text-align: left;
|
|
482
|
+
font-family: var(--mono-font), monospace;
|
|
483
|
+
font-size: 12px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.f-name bdi {
|
|
487
|
+
unicode-bidi: plaintext;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.f-right {
|
|
491
|
+
flex: none;
|
|
492
|
+
display: flex;
|
|
493
|
+
flex-direction: column;
|
|
494
|
+
align-items: flex-end;
|
|
495
|
+
gap: 2px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.f-counts {
|
|
499
|
+
font-size: 10.5px;
|
|
500
|
+
color: var(--fg-dim);
|
|
501
|
+
font-family: var(--mono-font), monospace;
|
|
502
|
+
white-space: nowrap;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.f-counts .plus {
|
|
506
|
+
color: var(--add-sign);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.f-counts .minus {
|
|
510
|
+
color: var(--del-sign);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.f-bar {
|
|
514
|
+
width: 64px;
|
|
515
|
+
height: 5px;
|
|
516
|
+
display: flex;
|
|
517
|
+
background: color-mix(in srgb, var(--fg) 14%, transparent);
|
|
518
|
+
border-radius: 2px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.f-bar .rev {
|
|
522
|
+
background: var(--file-neon);
|
|
523
|
+
height: 100%;
|
|
524
|
+
border-radius: 2px;
|
|
525
|
+
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--file-neon) 75%, transparent));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.f-bar .seen {
|
|
529
|
+
background: color-mix(in srgb, var(--file-neon) 40%, transparent);
|
|
530
|
+
height: 100%;
|
|
531
|
+
border-radius: 2px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#main {
|
|
535
|
+
flex: 1;
|
|
536
|
+
display: flex;
|
|
537
|
+
flex-direction: column;
|
|
538
|
+
min-width: 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#topbar {
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
gap: 10px;
|
|
545
|
+
padding: 6px 12px;
|
|
546
|
+
border-bottom: 1px solid var(--border);
|
|
547
|
+
background: var(--bg2);
|
|
548
|
+
flex: none;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#tb-path {
|
|
552
|
+
font-family: var(--mono-font), monospace;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
overflow: hidden;
|
|
555
|
+
text-overflow: ellipsis;
|
|
556
|
+
white-space: nowrap;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#tb-info {
|
|
560
|
+
color: var(--fg-dim);
|
|
561
|
+
font-size: 12px;
|
|
562
|
+
white-space: nowrap;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
#tb-sp {
|
|
566
|
+
flex: 1;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#topbar button {
|
|
570
|
+
font-size: 12px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#pane-wrap {
|
|
574
|
+
flex: 1;
|
|
575
|
+
display: flex;
|
|
576
|
+
min-height: 0;
|
|
577
|
+
position: relative;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#pane {
|
|
581
|
+
flex: 1;
|
|
582
|
+
min-width: 0;
|
|
583
|
+
overflow: auto;
|
|
584
|
+
position: relative;
|
|
585
|
+
font-family: var(--mono-font), monospace;
|
|
586
|
+
font-size: var(--font-size);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#spacer {
|
|
590
|
+
position: relative;
|
|
591
|
+
min-width: 100%;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.row {
|
|
595
|
+
position: absolute;
|
|
596
|
+
left: 0;
|
|
597
|
+
right: 0;
|
|
598
|
+
line-height: var(--line-height);
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: flex-start;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.row.add {
|
|
604
|
+
background: var(--add-bg);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.row.del {
|
|
608
|
+
background: var(--del-bg);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.row.focused-block {
|
|
612
|
+
box-shadow: inset 2px 0 0 var(--focus-ring);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.row.selected .ln.new,
|
|
616
|
+
.row.selected .content {
|
|
617
|
+
background: var(--sel-bg);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.ln {
|
|
621
|
+
flex: none;
|
|
622
|
+
width: 4.5em;
|
|
623
|
+
padding-right: 8px;
|
|
624
|
+
text-align: right;
|
|
625
|
+
color: var(--fg-dim);
|
|
626
|
+
cursor: pointer;
|
|
627
|
+
user-select: none;
|
|
628
|
+
font-size: 11px;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.ln:hover {
|
|
632
|
+
color: var(--accent);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.sign {
|
|
636
|
+
flex: none;
|
|
637
|
+
width: 1.2em;
|
|
638
|
+
text-align: center;
|
|
639
|
+
user-select: none;
|
|
640
|
+
font-weight: 700;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.row.add .sign {
|
|
644
|
+
color: var(--add-sign);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.row.del .sign {
|
|
648
|
+
color: var(--del-sign);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.chk {
|
|
652
|
+
flex: none;
|
|
653
|
+
width: 1.4em;
|
|
654
|
+
text-align: center;
|
|
655
|
+
cursor: pointer;
|
|
656
|
+
user-select: none;
|
|
657
|
+
color: var(--fg-dim);
|
|
658
|
+
font-size: 14px;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.chk.seen {
|
|
662
|
+
color: color-mix(in srgb, var(--add-sign) 60%, var(--fg-dim));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.chk.reviewed {
|
|
666
|
+
color: var(--add-sign);
|
|
667
|
+
font-weight: 700;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.chk:hover {
|
|
671
|
+
color: var(--accent);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.content {
|
|
675
|
+
flex: 1;
|
|
676
|
+
min-width: 0;
|
|
677
|
+
font-family: inherit;
|
|
678
|
+
font-size: inherit;
|
|
679
|
+
white-space: pre-wrap;
|
|
680
|
+
word-break: break-all;
|
|
681
|
+
tab-size: 4;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.row.split {
|
|
685
|
+
align-items: stretch;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.row.split .half {
|
|
689
|
+
flex: 1 1 0;
|
|
690
|
+
min-width: 0;
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: flex-start;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.row.split .half.left {
|
|
696
|
+
border-right: 1px solid var(--border);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.row.split .half.add {
|
|
700
|
+
background: var(--add-bg);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.row.split .half.del {
|
|
704
|
+
background: var(--del-bg);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.row.split .half.empty {
|
|
708
|
+
background: color-mix(in srgb, var(--fg) 5%, transparent);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.row.split .half.add .sign {
|
|
712
|
+
color: var(--add-sign);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.row.split .half.del .sign {
|
|
716
|
+
color: var(--del-sign);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.row.split.selected .half {
|
|
720
|
+
background: var(--sel-bg);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
#empty-msg {
|
|
724
|
+
padding: 48px;
|
|
725
|
+
color: var(--fg-dim);
|
|
726
|
+
font-size: 14px;
|
|
727
|
+
font-family: system-ui, sans-serif;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#wt-bar {
|
|
731
|
+
flex: none;
|
|
732
|
+
height: var(--wt-height, 160px);
|
|
733
|
+
background: var(--bg2);
|
|
734
|
+
border-top: 1px solid var(--border);
|
|
735
|
+
display: flex;
|
|
736
|
+
flex-direction: column;
|
|
737
|
+
position: relative;
|
|
738
|
+
font-family: system-ui, sans-serif;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
#wt-bar[hidden] {
|
|
742
|
+
display: none;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
#wt-resizer {
|
|
746
|
+
position: absolute;
|
|
747
|
+
top: -3px;
|
|
748
|
+
left: 0;
|
|
749
|
+
right: 0;
|
|
750
|
+
height: 6px;
|
|
751
|
+
cursor: row-resize;
|
|
752
|
+
z-index: 5;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
#wt-resizer:hover,
|
|
756
|
+
#wt-resizer.active {
|
|
757
|
+
background: var(--accent);
|
|
758
|
+
opacity: .5;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
#wt-head {
|
|
762
|
+
display: flex;
|
|
763
|
+
align-items: baseline;
|
|
764
|
+
gap: 10px;
|
|
765
|
+
padding: 8px 14px 2px;
|
|
766
|
+
flex: none;
|
|
767
|
+
min-width: 0;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#wt-act {
|
|
771
|
+
color: var(--accent);
|
|
772
|
+
font-size: 11px;
|
|
773
|
+
font-weight: 700;
|
|
774
|
+
letter-spacing: .08em;
|
|
775
|
+
text-transform: uppercase;
|
|
776
|
+
white-space: nowrap;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
#wt-count {
|
|
780
|
+
color: var(--fg-dim);
|
|
781
|
+
font-size: 11px;
|
|
782
|
+
white-space: nowrap;
|
|
783
|
+
font-family: var(--mono-font), monospace;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
#wt-title {
|
|
787
|
+
font-weight: 600;
|
|
788
|
+
overflow: hidden;
|
|
789
|
+
text-overflow: ellipsis;
|
|
790
|
+
white-space: nowrap;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
#wt-tag {
|
|
794
|
+
font-size: 10px;
|
|
795
|
+
color: var(--fg-dim);
|
|
796
|
+
border: 1px solid var(--border);
|
|
797
|
+
border-radius: 3px;
|
|
798
|
+
padding: 0 4px;
|
|
799
|
+
display: none;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
#wt-bar.d1 #wt-tag {
|
|
803
|
+
display: inline;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
#wt-bar.d1 #wt-head,
|
|
807
|
+
#wt-bar.d1 #wt-text,
|
|
808
|
+
#wt-bar.d1 #wt-refs {
|
|
809
|
+
padding-left: 28px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
#wt-sp {
|
|
813
|
+
flex: 1;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
#wt-hint {
|
|
817
|
+
color: var(--fg-dim);
|
|
818
|
+
font-size: 11px;
|
|
819
|
+
white-space: nowrap;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
#wt-dismiss {
|
|
823
|
+
font-size: 11px;
|
|
824
|
+
white-space: nowrap;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
#wt-text {
|
|
828
|
+
flex: 1;
|
|
829
|
+
overflow-y: auto;
|
|
830
|
+
padding: 4px 14px;
|
|
831
|
+
font-size: 13px;
|
|
832
|
+
line-height: 1.5;
|
|
833
|
+
min-height: 0;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
#wt-text.err {
|
|
837
|
+
color: var(--del-sign);
|
|
838
|
+
white-space: pre-wrap;
|
|
839
|
+
font-family: var(--mono-font), monospace;
|
|
840
|
+
font-size: 12px;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
#wt-refs {
|
|
844
|
+
flex: none;
|
|
845
|
+
display: flex;
|
|
846
|
+
gap: 6px;
|
|
847
|
+
padding: 4px 14px;
|
|
848
|
+
flex-wrap: wrap;
|
|
849
|
+
align-items: center;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.wt-chip {
|
|
853
|
+
font-family: var(--mono-font), monospace;
|
|
854
|
+
font-size: 11px;
|
|
855
|
+
border: 1px solid var(--border);
|
|
856
|
+
border-radius: 4px;
|
|
857
|
+
padding: 1px 7px;
|
|
858
|
+
cursor: pointer;
|
|
859
|
+
color: var(--fg);
|
|
860
|
+
background: var(--bg);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.wt-chip:hover {
|
|
864
|
+
border-color: var(--accent);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.wt-chip.primary {
|
|
868
|
+
border-color: var(--accent);
|
|
869
|
+
color: var(--accent);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.wt-fwd {
|
|
873
|
+
font-size: 11px;
|
|
874
|
+
color: var(--fg-dim);
|
|
875
|
+
cursor: pointer;
|
|
876
|
+
margin-left: auto;
|
|
877
|
+
white-space: nowrap;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.wt-fwd:hover {
|
|
881
|
+
color: var(--accent);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
#wt-pips {
|
|
885
|
+
flex: none;
|
|
886
|
+
display: flex;
|
|
887
|
+
align-items: center;
|
|
888
|
+
gap: 4px;
|
|
889
|
+
padding: 6px 14px 8px;
|
|
890
|
+
overflow-x: auto;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.wt-pip {
|
|
894
|
+
width: 8px;
|
|
895
|
+
height: 8px;
|
|
896
|
+
border-radius: 50%;
|
|
897
|
+
flex: none;
|
|
898
|
+
cursor: pointer;
|
|
899
|
+
border: 1px solid var(--fg-dim);
|
|
900
|
+
background: transparent;
|
|
901
|
+
opacity: .45;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.wt-pip.visited {
|
|
905
|
+
opacity: 1;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.wt-pip.cur {
|
|
909
|
+
background: var(--accent);
|
|
910
|
+
border-color: var(--accent);
|
|
911
|
+
opacity: 1;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.wt-pip.d1 {
|
|
915
|
+
width: 5px;
|
|
916
|
+
height: 5px;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.wt-gap {
|
|
920
|
+
width: 8px;
|
|
921
|
+
flex: none;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.row.wt-hi {
|
|
925
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
926
|
+
box-shadow: inset 2px 0 0 var(--accent);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.row.split.wt-hi .half {
|
|
930
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.row.wt-anchor .ln.new {
|
|
934
|
+
position: relative;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.row.wt-anchor .ln.new::before {
|
|
938
|
+
content: "";
|
|
939
|
+
position: absolute;
|
|
940
|
+
right: 1px;
|
|
941
|
+
top: 50%;
|
|
942
|
+
margin-top: -3px;
|
|
943
|
+
width: 6px;
|
|
944
|
+
height: 6px;
|
|
945
|
+
border-radius: 50%;
|
|
946
|
+
background: var(--accent);
|
|
947
|
+
opacity: .5;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
#wt-outline {
|
|
951
|
+
border-bottom: 1px solid var(--border);
|
|
952
|
+
max-height: 40%;
|
|
953
|
+
overflow-y: auto;
|
|
954
|
+
flex: none;
|
|
955
|
+
font-size: 12px;
|
|
956
|
+
padding-bottom: 4px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
#wt-o-head {
|
|
960
|
+
padding: 7px 12px 3px;
|
|
961
|
+
font-weight: 600;
|
|
962
|
+
cursor: pointer;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
#wt-o-head .d-arrow {
|
|
966
|
+
display: inline-block;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.wt-o-act {
|
|
970
|
+
padding: 5px 12px 2px;
|
|
971
|
+
color: var(--fg-dim);
|
|
972
|
+
font-size: 10.5px;
|
|
973
|
+
font-weight: 700;
|
|
974
|
+
letter-spacing: .06em;
|
|
975
|
+
text-transform: uppercase;
|
|
976
|
+
cursor: pointer;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
.wt-o-step {
|
|
980
|
+
padding: 2px 12px 2px 22px;
|
|
981
|
+
cursor: pointer;
|
|
982
|
+
overflow: hidden;
|
|
983
|
+
text-overflow: ellipsis;
|
|
984
|
+
white-space: nowrap;
|
|
985
|
+
border-left: 2px solid transparent;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.wt-o-step:hover {
|
|
989
|
+
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.wt-o-step.cur {
|
|
993
|
+
border-left-color: var(--accent);
|
|
994
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.wt-o-step.d1 {
|
|
998
|
+
padding-left: 32px;
|
|
999
|
+
color: var(--fg-dim);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.wt-files-head {
|
|
1003
|
+
padding: 8px 12px 2px;
|
|
1004
|
+
color: var(--fg-dim);
|
|
1005
|
+
font-size: 10.5px;
|
|
1006
|
+
font-weight: 700;
|
|
1007
|
+
letter-spacing: .06em;
|
|
1008
|
+
text-transform: uppercase;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
#searchbar {
|
|
1012
|
+
flex: none;
|
|
1013
|
+
display: flex;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
gap: 6px;
|
|
1016
|
+
padding: 5px 12px;
|
|
1017
|
+
background: var(--bg2);
|
|
1018
|
+
border-bottom: 1px solid var(--border);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
#searchbar[hidden] {
|
|
1022
|
+
display: none;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
#s-input {
|
|
1026
|
+
flex: 1;
|
|
1027
|
+
min-width: 0;
|
|
1028
|
+
padding: 3px 8px;
|
|
1029
|
+
background: var(--bg);
|
|
1030
|
+
color: var(--fg);
|
|
1031
|
+
border: 1px solid var(--border);
|
|
1032
|
+
border-radius: 4px;
|
|
1033
|
+
font-family: var(--mono-font), monospace;
|
|
1034
|
+
font-size: 12px;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
#s-input:focus {
|
|
1038
|
+
outline: 1px solid var(--accent);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
#s-input.bad {
|
|
1042
|
+
border-color: var(--del-sign);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
#s-count {
|
|
1046
|
+
color: var(--fg-dim);
|
|
1047
|
+
font-size: 11.5px;
|
|
1048
|
+
white-space: nowrap;
|
|
1049
|
+
font-family: var(--mono-font), monospace;
|
|
1050
|
+
min-width: 7em;
|
|
1051
|
+
text-align: right;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
#searchbar button {
|
|
1055
|
+
font-size: 11.5px;
|
|
1056
|
+
padding: 1px 7px;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
#searchbar button.on {
|
|
1060
|
+
border-color: var(--accent);
|
|
1061
|
+
color: var(--accent);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
mark {
|
|
1065
|
+
background: color-mix(in srgb, var(--mixed) 35%, transparent);
|
|
1066
|
+
color: inherit;
|
|
1067
|
+
border-radius: 2px;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
mark.cur {
|
|
1071
|
+
background: color-mix(in srgb, var(--mixed) 65%, transparent);
|
|
1072
|
+
box-shadow: 0 0 0 1px var(--mixed);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
#s-panel {
|
|
1076
|
+
width: 320px;
|
|
1077
|
+
min-width: 180px;
|
|
1078
|
+
max-width: 50vw;
|
|
1079
|
+
flex: none;
|
|
1080
|
+
background: var(--bg2);
|
|
1081
|
+
border-left: 1px solid var(--border);
|
|
1082
|
+
display: flex;
|
|
1083
|
+
flex-direction: column;
|
|
1084
|
+
position: relative;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
#s-panel[hidden] {
|
|
1088
|
+
display: none;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
#sp-resizer {
|
|
1092
|
+
position: absolute;
|
|
1093
|
+
top: 0;
|
|
1094
|
+
left: -3px;
|
|
1095
|
+
width: 6px;
|
|
1096
|
+
height: 100%;
|
|
1097
|
+
cursor: col-resize;
|
|
1098
|
+
z-index: 5;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
#sp-resizer:hover,
|
|
1102
|
+
#sp-resizer.active {
|
|
1103
|
+
background: var(--accent);
|
|
1104
|
+
opacity: .5;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
#sp-head {
|
|
1108
|
+
display: flex;
|
|
1109
|
+
align-items: baseline;
|
|
1110
|
+
gap: 8px;
|
|
1111
|
+
flex: none;
|
|
1112
|
+
padding: 8px 10px 6px;
|
|
1113
|
+
border-bottom: 1px solid var(--border);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
#sp-title {
|
|
1117
|
+
font-weight: 600;
|
|
1118
|
+
font-size: 12.5px;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
#sp-sub {
|
|
1122
|
+
color: var(--fg-dim);
|
|
1123
|
+
font-size: 11px;
|
|
1124
|
+
flex: 1;
|
|
1125
|
+
min-width: 0;
|
|
1126
|
+
overflow: hidden;
|
|
1127
|
+
text-overflow: ellipsis;
|
|
1128
|
+
white-space: nowrap;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
#sp-close {
|
|
1132
|
+
font-size: 11px;
|
|
1133
|
+
flex: none;
|
|
1134
|
+
padding: 0 5px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
#s-results {
|
|
1138
|
+
overflow-y: auto;
|
|
1139
|
+
flex: 1;
|
|
1140
|
+
padding-bottom: 12px;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.sr-file {
|
|
1144
|
+
padding: 7px 10px 2px;
|
|
1145
|
+
font-family: var(--mono-font), monospace;
|
|
1146
|
+
font-size: 11.5px;
|
|
1147
|
+
color: var(--fg-dim);
|
|
1148
|
+
overflow: hidden;
|
|
1149
|
+
text-overflow: ellipsis;
|
|
1150
|
+
white-space: nowrap;
|
|
1151
|
+
direction: rtl;
|
|
1152
|
+
text-align: left;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.sr-file bdi {
|
|
1156
|
+
unicode-bidi: plaintext;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.sr-hit {
|
|
1160
|
+
display: flex;
|
|
1161
|
+
gap: 6px;
|
|
1162
|
+
padding: 1px 10px 1px 14px;
|
|
1163
|
+
cursor: pointer;
|
|
1164
|
+
border-left: 2px solid transparent;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.sr-hit:hover {
|
|
1168
|
+
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.sr-hit.cur {
|
|
1172
|
+
border-left-color: var(--accent);
|
|
1173
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.sr-ln {
|
|
1177
|
+
flex: none;
|
|
1178
|
+
width: 3.6em;
|
|
1179
|
+
text-align: right;
|
|
1180
|
+
color: var(--fg-dim);
|
|
1181
|
+
font-family: var(--mono-font), monospace;
|
|
1182
|
+
font-size: 10.5px;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.sr-hit.add .sr-ln {
|
|
1186
|
+
color: var(--add-sign);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.sr-hit.del .sr-ln {
|
|
1190
|
+
color: var(--del-sign);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.sr-txt {
|
|
1194
|
+
flex: 1;
|
|
1195
|
+
min-width: 0;
|
|
1196
|
+
overflow: hidden;
|
|
1197
|
+
text-overflow: ellipsis;
|
|
1198
|
+
white-space: nowrap;
|
|
1199
|
+
font-family: var(--mono-font), monospace;
|
|
1200
|
+
font-size: 11.5px;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
#s-note {
|
|
1204
|
+
padding: 14px 12px;
|
|
1205
|
+
color: var(--fg-dim);
|
|
1206
|
+
font-size: 12px;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
#minimap {
|
|
1210
|
+
flex: none;
|
|
1211
|
+
width: 14px;
|
|
1212
|
+
cursor: pointer;
|
|
1213
|
+
background: var(--bg2);
|
|
1214
|
+
border-left: 1px solid var(--border);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
#help-overlay {
|
|
1218
|
+
position: fixed;
|
|
1219
|
+
inset: 0;
|
|
1220
|
+
background: rgba(0, 0, 0, .45);
|
|
1221
|
+
display: none;
|
|
1222
|
+
align-items: center;
|
|
1223
|
+
justify-content: center;
|
|
1224
|
+
z-index: 20;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
#help-overlay.show {
|
|
1228
|
+
display: flex;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
#help {
|
|
1232
|
+
background: var(--bg2);
|
|
1233
|
+
border: 1px solid var(--border);
|
|
1234
|
+
border-radius: 8px;
|
|
1235
|
+
padding: 18px 22px;
|
|
1236
|
+
max-width: 480px;
|
|
1237
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, .35);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
#help-title {
|
|
1241
|
+
font-weight: 600;
|
|
1242
|
+
margin-bottom: 10px;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
#help table {
|
|
1246
|
+
border-collapse: collapse;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
#help td {
|
|
1250
|
+
padding: 3px 8px;
|
|
1251
|
+
vertical-align: top;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
#help td:first-child {
|
|
1255
|
+
font-family: var(--mono-font), monospace;
|
|
1256
|
+
font-size: 12px;
|
|
1257
|
+
color: var(--accent);
|
|
1258
|
+
white-space: nowrap;
|
|
1259
|
+
text-align: right;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
#help-hint {
|
|
1263
|
+
margin-top: 10px;
|
|
1264
|
+
color: var(--fg-dim);
|
|
1265
|
+
font-size: 12px;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
#confirm-overlay {
|
|
1269
|
+
position: fixed;
|
|
1270
|
+
inset: 0;
|
|
1271
|
+
background: rgba(0, 0, 0, .45);
|
|
1272
|
+
display: none;
|
|
1273
|
+
align-items: center;
|
|
1274
|
+
justify-content: center;
|
|
1275
|
+
z-index: 21;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
#confirm-overlay.show {
|
|
1279
|
+
display: flex;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
#confirm {
|
|
1283
|
+
background: var(--bg2);
|
|
1284
|
+
border: 1px solid var(--border);
|
|
1285
|
+
border-radius: 8px;
|
|
1286
|
+
padding: 18px 22px;
|
|
1287
|
+
max-width: 420px;
|
|
1288
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, .35);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
#confirm-btns {
|
|
1292
|
+
margin-top: 14px;
|
|
1293
|
+
display: flex;
|
|
1294
|
+
gap: 10px;
|
|
1295
|
+
justify-content: flex-end;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
#confirm-yes {
|
|
1299
|
+
border-color: var(--del-sign);
|
|
1300
|
+
color: var(--del-sign);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
#toast {
|
|
1304
|
+
position: fixed;
|
|
1305
|
+
bottom: 24px;
|
|
1306
|
+
left: 50%;
|
|
1307
|
+
transform: translateX(-50%);
|
|
1308
|
+
background: var(--toast-bg);
|
|
1309
|
+
color: var(--toast-fg);
|
|
1310
|
+
padding: 8px 16px;
|
|
1311
|
+
border-radius: 6px;
|
|
1312
|
+
font-size: 12.5px;
|
|
1313
|
+
opacity: 0;
|
|
1314
|
+
transition: opacity .18s;
|
|
1315
|
+
pointer-events: none;
|
|
1316
|
+
max-width: 70vw;
|
|
1317
|
+
overflow: hidden;
|
|
1318
|
+
text-overflow: ellipsis;
|
|
1319
|
+
white-space: nowrap;
|
|
1320
|
+
z-index: 10;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
#toast.show {
|
|
1324
|
+
opacity: 1;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
#toast input {
|
|
1328
|
+
pointer-events: auto;
|
|
1329
|
+
font: inherit;
|
|
1330
|
+
width: 320px;
|
|
1331
|
+
}
|
|
1332
|
+
</style>
|
|
1333
|
+
</head>
|
|
1334
|
+
|
|
1335
|
+
<body>
|
|
1336
|
+
<nav id="sidebar">
|
|
1337
|
+
<div id="sb-head">
|
|
1338
|
+
<div id="sb-title"><span class="wm-seen">seen</span><span class="wm-diff">diff</span></div>
|
|
1339
|
+
<div id="sb-stats">loading…</div>
|
|
1340
|
+
<div id="sb-progress"><span class="rev"></span><span class="seen"></span></div>
|
|
1341
|
+
<div id="sb-base"></div>
|
|
1342
|
+
<div id="dirty-banner"></div>
|
|
1343
|
+
</div>
|
|
1344
|
+
<div id="wt-outline" hidden></div>
|
|
1345
|
+
<input id="filter" placeholder="filter files (g)" spellcheck="false">
|
|
1346
|
+
<div id="filelist"></div>
|
|
1347
|
+
<div id="sb-resizer" title="drag to resize"></div>
|
|
1348
|
+
</nav>
|
|
1349
|
+
<main id="main">
|
|
1350
|
+
<div id="topbar">
|
|
1351
|
+
<span id="tb-path">no file open</span>
|
|
1352
|
+
<span id="tb-info"></span>
|
|
1353
|
+
<span id="tb-sp"></span>
|
|
1354
|
+
<button id="btn-split" title="side-by-side / unified (s)">unified</button>
|
|
1355
|
+
<button id="btn-hide" title="hide fully-reviewed files (h)">hide ✓</button>
|
|
1356
|
+
<button id="btn-font" title="toggle font"></button>
|
|
1357
|
+
<button id="btn-theme" title="cycle theme (t)"></button>
|
|
1358
|
+
<button id="btn-keys" title="keyboard shortcuts (?)">keys?</button>
|
|
1359
|
+
</div>
|
|
1360
|
+
<div id="searchbar" hidden>
|
|
1361
|
+
<input id="s-input" placeholder="search all changed files" spellcheck="false">
|
|
1362
|
+
<span id="s-count"></span>
|
|
1363
|
+
<button id="s-prev" title="previous match (⇧⏎)">↑</button>
|
|
1364
|
+
<button id="s-next" title="next match (⏎)">↓</button>
|
|
1365
|
+
<button id="s-regex" title="regular expression">.*</button>
|
|
1366
|
+
<button id="s-panel-btn" title="show / hide the results panel">results</button>
|
|
1367
|
+
<button id="s-close" title="close search (Esc)">✕</button>
|
|
1368
|
+
</div>
|
|
1369
|
+
<div id="pane-wrap">
|
|
1370
|
+
<div id="pane">
|
|
1371
|
+
<div id="spacer"></div>
|
|
1372
|
+
</div>
|
|
1373
|
+
<canvas id="minimap"></canvas>
|
|
1374
|
+
</div>
|
|
1375
|
+
<div id="wt-bar" hidden>
|
|
1376
|
+
<div id="wt-resizer" title="drag to resize"></div>
|
|
1377
|
+
<div id="wt-head">
|
|
1378
|
+
<span id="wt-act"></span><span id="wt-count"></span>
|
|
1379
|
+
<span id="wt-title"></span><span id="wt-tag">detail</span>
|
|
1380
|
+
<span id="wt-sp"></span>
|
|
1381
|
+
<span id="wt-hint">← → step · [ ] act · . detail · w hide</span>
|
|
1382
|
+
<button id="wt-dismiss" title="dismiss this walkthrough — back to the plain diff">dismiss ✕</button>
|
|
1383
|
+
</div>
|
|
1384
|
+
<div id="wt-text"></div>
|
|
1385
|
+
<div id="wt-refs"></div>
|
|
1386
|
+
<div id="wt-pips"></div>
|
|
1387
|
+
</div>
|
|
1388
|
+
</main>
|
|
1389
|
+
<aside id="s-panel" hidden>
|
|
1390
|
+
<div id="sp-resizer" title="drag to resize"></div>
|
|
1391
|
+
<div id="sp-head">
|
|
1392
|
+
<span id="sp-title">results</span>
|
|
1393
|
+
<span id="sp-sub"></span>
|
|
1394
|
+
<button id="sp-close" title="dismiss the results panel">✕</button>
|
|
1395
|
+
</div>
|
|
1396
|
+
<div id="s-results"></div>
|
|
1397
|
+
</aside>
|
|
1398
|
+
<div id="help-overlay">
|
|
1399
|
+
<div id="help">
|
|
1400
|
+
<div id="help-title">Keyboard shortcuts</div>
|
|
1401
|
+
<table>
|
|
1402
|
+
<tr>
|
|
1403
|
+
<td>← / →</td>
|
|
1404
|
+
<td>prev / next change block in this file</td>
|
|
1405
|
+
</tr>
|
|
1406
|
+
<tr>
|
|
1407
|
+
<td>↑ / ↓</td>
|
|
1408
|
+
<td>prev / next file</td>
|
|
1409
|
+
</tr>
|
|
1410
|
+
<tr>
|
|
1411
|
+
<td>R</td>
|
|
1412
|
+
<td>mark block reviewed, advance (toggles off if reviewed)</td>
|
|
1413
|
+
</tr>
|
|
1414
|
+
<tr>
|
|
1415
|
+
<td>n / N</td>
|
|
1416
|
+
<td>next / prev unreviewed block, crosses files</td>
|
|
1417
|
+
</tr>
|
|
1418
|
+
<tr>
|
|
1419
|
+
<td>j / k</td>
|
|
1420
|
+
<td>scroll lines</td>
|
|
1421
|
+
</tr>
|
|
1422
|
+
<tr>
|
|
1423
|
+
<td>d / u</td>
|
|
1424
|
+
<td>scroll half page</td>
|
|
1425
|
+
</tr>
|
|
1426
|
+
<tr>
|
|
1427
|
+
<td>V</td>
|
|
1428
|
+
<td>mark all seen-but-unreviewed blocks in file reviewed</td>
|
|
1429
|
+
</tr>
|
|
1430
|
+
<tr>
|
|
1431
|
+
<td>f</td>
|
|
1432
|
+
<td>mark whole file reviewed, next file</td>
|
|
1433
|
+
</tr>
|
|
1434
|
+
<tr>
|
|
1435
|
+
<td>y</td>
|
|
1436
|
+
<td>copy ref + content (selection or focused block)</td>
|
|
1437
|
+
</tr>
|
|
1438
|
+
<tr>
|
|
1439
|
+
<td>Y</td>
|
|
1440
|
+
<td>copy refs for all unreviewed blocks in file</td>
|
|
1441
|
+
</tr>
|
|
1442
|
+
<tr>
|
|
1443
|
+
<td>s</td>
|
|
1444
|
+
<td>toggle side-by-side / unified</td>
|
|
1445
|
+
</tr>
|
|
1446
|
+
<tr>
|
|
1447
|
+
<td>Ctrl-F</td>
|
|
1448
|
+
<td>search all changed files — ⏎ / ⇧⏎ next / prev · Esc closes</td>
|
|
1449
|
+
</tr>
|
|
1450
|
+
<tr>
|
|
1451
|
+
<td>g</td>
|
|
1452
|
+
<td>focus file filter</td>
|
|
1453
|
+
</tr>
|
|
1454
|
+
<tr>
|
|
1455
|
+
<td>h</td>
|
|
1456
|
+
<td>hide fully-reviewed files</td>
|
|
1457
|
+
</tr>
|
|
1458
|
+
<tr>
|
|
1459
|
+
<td>t</td>
|
|
1460
|
+
<td>cycle theme</td>
|
|
1461
|
+
</tr>
|
|
1462
|
+
<tr>
|
|
1463
|
+
<td>X</td>
|
|
1464
|
+
<td>reset all seen / reviewed state (asks first)</td>
|
|
1465
|
+
</tr>
|
|
1466
|
+
<tr>
|
|
1467
|
+
<td>?</td>
|
|
1468
|
+
<td>this help · Esc closes</td>
|
|
1469
|
+
</tr>
|
|
1470
|
+
<tbody id="help-wt" hidden>
|
|
1471
|
+
<tr>
|
|
1472
|
+
<td colspan="2" style="padding-top:10px;font-weight:600">Walkthrough</td>
|
|
1473
|
+
</tr>
|
|
1474
|
+
<tr>
|
|
1475
|
+
<td>w</td>
|
|
1476
|
+
<td>enter / exit walkthrough mode</td>
|
|
1477
|
+
</tr>
|
|
1478
|
+
<tr>
|
|
1479
|
+
<td>← / →</td>
|
|
1480
|
+
<td>prev / next step (in walkthrough mode)</td>
|
|
1481
|
+
</tr>
|
|
1482
|
+
<tr>
|
|
1483
|
+
<td>[ / ]</td>
|
|
1484
|
+
<td>prev / next act</td>
|
|
1485
|
+
</tr>
|
|
1486
|
+
<tr>
|
|
1487
|
+
<td>1–9</td>
|
|
1488
|
+
<td>jump to act <i>n</i></td>
|
|
1489
|
+
</tr>
|
|
1490
|
+
<tr>
|
|
1491
|
+
<td>.</td>
|
|
1492
|
+
<td>show / hide detail (depth-1) steps</td>
|
|
1493
|
+
</tr>
|
|
1494
|
+
<tr>
|
|
1495
|
+
<td>⇧← / ⇧→</td>
|
|
1496
|
+
<td>prev / next change block (escape hatch)</td>
|
|
1497
|
+
</tr>
|
|
1498
|
+
</tbody>
|
|
1499
|
+
</table>
|
|
1500
|
+
<div id="help-hint">click a line number to copy a ref · shift-click / drag for a range</div>
|
|
1501
|
+
</div>
|
|
1502
|
+
</div>
|
|
1503
|
+
<div id="confirm-overlay">
|
|
1504
|
+
<div id="confirm">
|
|
1505
|
+
<div id="confirm-msg"></div>
|
|
1506
|
+
<div id="confirm-btns">
|
|
1507
|
+
<button id="confirm-yes">Reset (Enter)</button>
|
|
1508
|
+
<button id="confirm-no">Cancel (Esc)</button>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
</div>
|
|
1512
|
+
<div id="toast"></div>
|
|
1513
|
+
|
|
1514
|
+
<script>
|
|
1515
|
+
"use strict";
|
|
1516
|
+
const $ = id => document.getElementById(id);
|
|
1517
|
+
function savePref(k, v) {
|
|
1518
|
+
localStorage.setItem(k, v);
|
|
1519
|
+
fetch("/api/prefs", {
|
|
1520
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1521
|
+
body: JSON.stringify({ key: k, value: String(v) }),
|
|
1522
|
+
}).catch(() => { });
|
|
1523
|
+
}
|
|
1524
|
+
const pane = $("pane"), spacer = $("spacer"), mm = $("minimap");
|
|
1525
|
+
const LH = parseFloat(getComputedStyle(document.documentElement)
|
|
1526
|
+
.getPropertyValue("--line-height"));
|
|
1527
|
+
const THEMES = ["light", "dark", "solarized-light", "solarized-dark",
|
|
1528
|
+
"space", "hailmary"];
|
|
1529
|
+
const FONTS = ["JetBrains Mono", "Ubuntu Sans Mono"];
|
|
1530
|
+
|
|
1531
|
+
let meta = null;
|
|
1532
|
+
let file = null;
|
|
1533
|
+
let blocks = [];
|
|
1534
|
+
let blockByRow = [];
|
|
1535
|
+
let curPath = null;
|
|
1536
|
+
let focusIdx = -1;
|
|
1537
|
+
let hideDone = localStorage.getItem("seendiff.hide") === "1";
|
|
1538
|
+
let selAnchor = null, selEnd = null;
|
|
1539
|
+
|
|
1540
|
+
let wt = null;
|
|
1541
|
+
let wtWatch = false;
|
|
1542
|
+
let wtStep = -1, wtOn = false, wtDetail = true, wtVisited = new Set();
|
|
1543
|
+
let wtHi = null;
|
|
1544
|
+
let wtAnchors = new Set();
|
|
1545
|
+
let wtAway = false;
|
|
1546
|
+
let wtActsClosed = new Set();
|
|
1547
|
+
|
|
1548
|
+
let split = localStorage.getItem("seendiff.split") === "1";
|
|
1549
|
+
let viewRows = [];
|
|
1550
|
+
let viewOf = [];
|
|
1551
|
+
let seenQueue = new Map();
|
|
1552
|
+
let dwell = new Map();
|
|
1553
|
+
|
|
1554
|
+
let allDone = null;
|
|
1555
|
+
function celebrate() {
|
|
1556
|
+
const cv = document.createElement("canvas");
|
|
1557
|
+
cv.id = "confetti";
|
|
1558
|
+
document.body.append(cv);
|
|
1559
|
+
const dpr = devicePixelRatio || 1;
|
|
1560
|
+
cv.width = innerWidth * dpr; cv.height = innerHeight * dpr;
|
|
1561
|
+
const ctx = cv.getContext("2d");
|
|
1562
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1563
|
+
const cs = getComputedStyle(document.documentElement);
|
|
1564
|
+
const colors = ["--add-sign", "--del-sign", "--mixed", "--accent", "--fg"]
|
|
1565
|
+
.map(v => cs.getPropertyValue(v).trim());
|
|
1566
|
+
const parts = [];
|
|
1567
|
+
for (let i = 0; i < 180; i++) {
|
|
1568
|
+
const fromLeft = i % 2 === 0;
|
|
1569
|
+
parts.push({
|
|
1570
|
+
x: fromLeft ? -10 : innerWidth + 10,
|
|
1571
|
+
y: innerHeight * (0.4 + Math.random() * 0.5),
|
|
1572
|
+
vx: (fromLeft ? 1 : -1) * (4 + Math.random() * 8),
|
|
1573
|
+
vy: -(7 + Math.random() * 7),
|
|
1574
|
+
w: 4 + Math.random() * 5, h: 6 + Math.random() * 6,
|
|
1575
|
+
rot: Math.random() * Math.PI, vr: (Math.random() - .5) * .3,
|
|
1576
|
+
color: colors[i % colors.length],
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
const t0 = performance.now();
|
|
1580
|
+
(function tick(t) {
|
|
1581
|
+
const dt = Math.min(2, (t - (tick.last || t0)) / 16.7); tick.last = t;
|
|
1582
|
+
ctx.clearRect(0, 0, innerWidth, innerHeight);
|
|
1583
|
+
for (const p of parts) {
|
|
1584
|
+
p.x += p.vx * dt; p.y += p.vy * dt;
|
|
1585
|
+
p.vy += 0.25 * dt; p.vx *= 0.99; p.rot += p.vr * dt;
|
|
1586
|
+
ctx.save();
|
|
1587
|
+
ctx.translate(p.x, p.y); ctx.rotate(p.rot);
|
|
1588
|
+
ctx.fillStyle = p.color;
|
|
1589
|
+
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
1590
|
+
ctx.restore();
|
|
1591
|
+
}
|
|
1592
|
+
if (t - t0 < 3200) requestAnimationFrame(tick);
|
|
1593
|
+
else cv.remove();
|
|
1594
|
+
})(t0);
|
|
1595
|
+
toast("🎉 all " + meta.stats.blocks + " blocks reviewed — ship it");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function toast(msg, inputText) {
|
|
1599
|
+
const t = $("toast");
|
|
1600
|
+
t.textContent = msg;
|
|
1601
|
+
if (inputText !== undefined) {
|
|
1602
|
+
const inp = document.createElement("input");
|
|
1603
|
+
inp.value = inputText;
|
|
1604
|
+
t.append(" ", inp);
|
|
1605
|
+
inp.focus(); inp.select();
|
|
1606
|
+
}
|
|
1607
|
+
t.classList.add("show");
|
|
1608
|
+
clearTimeout(toast._h);
|
|
1609
|
+
toast._h = setTimeout(() => t.classList.remove("show"), inputText ? 4000 : 1600);
|
|
1610
|
+
}
|
|
1611
|
+
async function copyText(text, what) {
|
|
1612
|
+
try { await navigator.clipboard.writeText(text); toast("copied " + what); }
|
|
1613
|
+
catch (e) { toast("copy manually:", text); }
|
|
1614
|
+
}
|
|
1615
|
+
async function api(method, url, body) {
|
|
1616
|
+
const r = await fetch(url, {
|
|
1617
|
+
method,
|
|
1618
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
1619
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
1620
|
+
});
|
|
1621
|
+
if (!r.ok) { toast("api error " + r.status); throw new Error(url); }
|
|
1622
|
+
return r.json();
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function fileGlyph(f) {
|
|
1626
|
+
if (f.reviewed >= f.blocks) return ["✓", "done"];
|
|
1627
|
+
if (f.reviewed > 0 || f.seen > 0) return ["◐", ""];
|
|
1628
|
+
return ["○", ""];
|
|
1629
|
+
}
|
|
1630
|
+
let collapsed = new Set(JSON.parse(localStorage.getItem("seendiff.collapsed") || "[]"));
|
|
1631
|
+
function saveCollapsed() {
|
|
1632
|
+
savePref("seendiff.collapsed", JSON.stringify([...collapsed]));
|
|
1633
|
+
}
|
|
1634
|
+
function makeBar(rev, seen, total) {
|
|
1635
|
+
const bar = document.createElement("span"); bar.className = "f-bar";
|
|
1636
|
+
const r = document.createElement("span"); r.className = "rev";
|
|
1637
|
+
r.style.width = (100 * rev / (total || 1)) + "%";
|
|
1638
|
+
const s = document.createElement("span"); s.className = "seen";
|
|
1639
|
+
s.style.width = (100 * seen / (total || 1)) + "%";
|
|
1640
|
+
bar.append(r, s);
|
|
1641
|
+
return bar;
|
|
1642
|
+
}
|
|
1643
|
+
let notch = { p: 5, pad: 3, n: 1 };
|
|
1644
|
+
function layoutNotches() {
|
|
1645
|
+
const dpr = devicePixelRatio || 1;
|
|
1646
|
+
const snap = n => Math.max(1, Math.round(n * dpr)) / dpr;
|
|
1647
|
+
const w = snap(7);
|
|
1648
|
+
notch.p = snap(10);
|
|
1649
|
+
notch.pad = snap(3);
|
|
1650
|
+
const avail = $("sb-head").clientWidth - 24 - 2 * notch.pad;
|
|
1651
|
+
notch.n = Math.max(1, Math.floor(avail / notch.p));
|
|
1652
|
+
const s = document.documentElement.style;
|
|
1653
|
+
s.setProperty("--notch-w", w + "px");
|
|
1654
|
+
s.setProperty("--notch-p", notch.p + "px");
|
|
1655
|
+
s.setProperty("--pbar-pad", notch.pad + "px");
|
|
1656
|
+
s.setProperty("--pbar-w", (notch.n * notch.p + 2 * notch.pad) + "px");
|
|
1657
|
+
}
|
|
1658
|
+
function setMainBar(rev, seen, total) {
|
|
1659
|
+
const bar = $("sb-progress");
|
|
1660
|
+
const lit = v => (!total || !v ? 0
|
|
1661
|
+
: Math.min(notch.n, Math.max(1, Math.round(v / total * notch.n)))
|
|
1662
|
+
* notch.p) + "px";
|
|
1663
|
+
bar.querySelector(".rev").style.width = lit(rev);
|
|
1664
|
+
bar.querySelector(".seen").style.width = lit(rev + seen);
|
|
1665
|
+
const done = total ? rev / total : 0;
|
|
1666
|
+
bar.style.setProperty("--glow",
|
|
1667
|
+
Math.min(1, Math.max(0, (done - 0.5) * 2)).toFixed(3));
|
|
1668
|
+
}
|
|
1669
|
+
function makeFileItem(f, label, indent) {
|
|
1670
|
+
const item = document.createElement("div");
|
|
1671
|
+
item.className = "f-item" + (f.path === curPath ? " cur" : "");
|
|
1672
|
+
item.style.paddingLeft = (8 + indent * 14) + "px";
|
|
1673
|
+
const [g, gcls] = fileGlyph(f);
|
|
1674
|
+
const glyph = document.createElement("span");
|
|
1675
|
+
glyph.className = "f-glyph " + gcls; glyph.textContent = g;
|
|
1676
|
+
glyph.title = "toggle whole file reviewed";
|
|
1677
|
+
glyph.onclick = e => { e.stopPropagation(); toggleFileReviewed(f); };
|
|
1678
|
+
const name = document.createElement("span");
|
|
1679
|
+
name.className = "f-name";
|
|
1680
|
+
const bdi = document.createElement("bdi");
|
|
1681
|
+
bdi.textContent = label;
|
|
1682
|
+
name.append(bdi);
|
|
1683
|
+
name.title = (f.old_path ? f.old_path + " → " : "") + f.path;
|
|
1684
|
+
const right = document.createElement("span"); right.className = "f-right";
|
|
1685
|
+
const counts = document.createElement("span"); counts.className = "f-counts";
|
|
1686
|
+
if (f.status === "binary" || f.status === "meta" || f.status === "submodule") {
|
|
1687
|
+
counts.textContent = f.status === "submodule" ? "⌥ pointer" : f.status;
|
|
1688
|
+
} else {
|
|
1689
|
+
const p = document.createElement("span"); p.className = "plus"; p.textContent = "+" + f.added;
|
|
1690
|
+
const m = document.createElement("span"); m.className = "minus"; m.textContent = "−" + f.removed;
|
|
1691
|
+
counts.append(p, " ", m);
|
|
1692
|
+
}
|
|
1693
|
+
right.append(counts, makeBar(f.reviewed_w, f.seen_w, f.weight));
|
|
1694
|
+
item.append(glyph, name, right);
|
|
1695
|
+
item.onclick = () => openFile(f.path);
|
|
1696
|
+
return item;
|
|
1697
|
+
}
|
|
1698
|
+
function buildTree(files) {
|
|
1699
|
+
const root = { dirs: new Map(), files: [] };
|
|
1700
|
+
for (const f of files) {
|
|
1701
|
+
const parts = f.path.split("/");
|
|
1702
|
+
let node = root;
|
|
1703
|
+
for (const part of parts.slice(0, -1)) {
|
|
1704
|
+
if (!node.dirs.has(part)) node.dirs.set(part, { dirs: new Map(), files: [] });
|
|
1705
|
+
node = node.dirs.get(part);
|
|
1706
|
+
}
|
|
1707
|
+
node.files.push(f);
|
|
1708
|
+
}
|
|
1709
|
+
function compress(node) {
|
|
1710
|
+
for (const [name, child] of [...node.dirs]) {
|
|
1711
|
+
compress(child);
|
|
1712
|
+
let n = name, c = child;
|
|
1713
|
+
while (c.files.length === 0 && c.dirs.size === 1) {
|
|
1714
|
+
const [[sub, subNode]] = c.dirs;
|
|
1715
|
+
n = n + "/" + sub; c = subNode;
|
|
1716
|
+
}
|
|
1717
|
+
if (n !== name) { node.dirs.delete(name); node.dirs.set(n, c); }
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
compress(root);
|
|
1721
|
+
return root;
|
|
1722
|
+
}
|
|
1723
|
+
function aggregate(node) {
|
|
1724
|
+
let a = { blocks: 0, seen: 0, reviewed: 0, weight: 0, seen_w: 0, reviewed_w: 0 };
|
|
1725
|
+
for (const f of node.files) {
|
|
1726
|
+
a.blocks += f.blocks; a.seen += f.seen; a.reviewed += f.reviewed;
|
|
1727
|
+
a.weight += f.weight; a.seen_w += f.seen_w; a.reviewed_w += f.reviewed_w;
|
|
1728
|
+
}
|
|
1729
|
+
for (const child of node.dirs.values()) {
|
|
1730
|
+
const c = aggregate(child);
|
|
1731
|
+
a.blocks += c.blocks; a.seen += c.seen; a.reviewed += c.reviewed;
|
|
1732
|
+
a.weight += c.weight; a.seen_w += c.seen_w; a.reviewed_w += c.reviewed_w;
|
|
1733
|
+
}
|
|
1734
|
+
node.agg = a;
|
|
1735
|
+
return a;
|
|
1736
|
+
}
|
|
1737
|
+
function renderTree(list, node, prefix, indent) {
|
|
1738
|
+
const dirs = [...node.dirs.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1739
|
+
for (const [name, child] of dirs) {
|
|
1740
|
+
const full = prefix + name;
|
|
1741
|
+
aggregate(child);
|
|
1742
|
+
if (hideDone && child.agg.reviewed >= child.agg.blocks) continue;
|
|
1743
|
+
const isClosed = collapsed.has(full);
|
|
1744
|
+
const item = document.createElement("div");
|
|
1745
|
+
item.className = "d-item";
|
|
1746
|
+
item.style.paddingLeft = (8 + indent * 14) + "px";
|
|
1747
|
+
const arrow = document.createElement("span");
|
|
1748
|
+
arrow.className = "d-arrow"; arrow.textContent = isClosed ? "▸" : "▾";
|
|
1749
|
+
const dn = document.createElement("span");
|
|
1750
|
+
dn.className = "d-name"; dn.textContent = name + "/"; dn.title = full;
|
|
1751
|
+
item.append(arrow, dn, makeBar(child.agg.reviewed_w, child.agg.seen_w, child.agg.weight));
|
|
1752
|
+
item.onclick = () => {
|
|
1753
|
+
isClosed ? collapsed.delete(full) : collapsed.add(full);
|
|
1754
|
+
saveCollapsed(); renderSidebar();
|
|
1755
|
+
};
|
|
1756
|
+
list.append(item);
|
|
1757
|
+
if (!isClosed) renderTree(list, child, full + "/", indent + 1);
|
|
1758
|
+
}
|
|
1759
|
+
for (const f of node.files.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
1760
|
+
if (hideDone && f.reviewed >= f.blocks) continue;
|
|
1761
|
+
list.append(makeFileItem(f, f.path.split("/").pop(), indent));
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function renderSidebar() {
|
|
1765
|
+
const q = $("filter").value.toLowerCase();
|
|
1766
|
+
const list = $("filelist");
|
|
1767
|
+
list.textContent = "";
|
|
1768
|
+
if (q) {
|
|
1769
|
+
for (const f of meta.files) {
|
|
1770
|
+
if (hideDone && f.reviewed >= f.blocks) continue;
|
|
1771
|
+
if (!f.path.toLowerCase().includes(q)) continue;
|
|
1772
|
+
list.append(makeFileItem(f, (f.old_path ? f.old_path + " → " : "") + f.path, 0));
|
|
1773
|
+
}
|
|
1774
|
+
} else {
|
|
1775
|
+
renderTree(list, buildTree(meta.files), "", 0);
|
|
1776
|
+
}
|
|
1777
|
+
if (wt && wt.wt_paths && wt.wt_paths.length) {
|
|
1778
|
+
const shown = wt.wt_paths.filter(p => !q || p.toLowerCase().includes(q));
|
|
1779
|
+
if (shown.length) {
|
|
1780
|
+
const head = document.createElement("div");
|
|
1781
|
+
head.className = "wt-files-head"; head.textContent = "walkthrough";
|
|
1782
|
+
list.append(head);
|
|
1783
|
+
for (const p of shown) {
|
|
1784
|
+
const item = document.createElement("div");
|
|
1785
|
+
item.className = "f-item" + (p === curPath ? " cur" : "");
|
|
1786
|
+
const glyph = document.createElement("span");
|
|
1787
|
+
glyph.className = "f-glyph"; glyph.textContent = "◈";
|
|
1788
|
+
const name = document.createElement("span");
|
|
1789
|
+
name.className = "f-name";
|
|
1790
|
+
const bdi = document.createElement("bdi");
|
|
1791
|
+
bdi.textContent = p; name.append(bdi); name.title = p;
|
|
1792
|
+
item.append(glyph, name);
|
|
1793
|
+
item.onclick = () => openFile(p);
|
|
1794
|
+
list.append(item);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const s = meta.stats;
|
|
1799
|
+
$("sb-stats").textContent =
|
|
1800
|
+
`${s.reviewed}/${s.blocks} reviewed · ${s.seen + s.reviewed} seen · ${meta.files.length} files`;
|
|
1801
|
+
setMainBar(s.reviewed_w, s.seen_w, s.weight);
|
|
1802
|
+
const done = s.blocks > 0 && s.reviewed >= s.blocks;
|
|
1803
|
+
if (allDone === null) allDone = done;
|
|
1804
|
+
else if (done && !allDone) { allDone = true; celebrate(); }
|
|
1805
|
+
else allDone = done;
|
|
1806
|
+
$("sb-base").textContent =
|
|
1807
|
+
`${meta.branch || "detached"} vs ${meta.base_ref} @ ${meta.base_sha} (${meta.base_date.slice(0, 10)})`;
|
|
1808
|
+
const dirty = meta.dirty_files.length, conf = meta.conflicted_files.length;
|
|
1809
|
+
const banner = $("dirty-banner");
|
|
1810
|
+
if (dirty || conf) {
|
|
1811
|
+
banner.style.display = "block";
|
|
1812
|
+
banner.textContent = (dirty ? `${dirty} file${dirty > 1 ? "s" : ""} with uncommitted changes, not shown` : "")
|
|
1813
|
+
+ (conf ? ` · ${conf} conflicted (skipped)` : "");
|
|
1814
|
+
banner.title = meta.dirty_files.concat(meta.conflicted_files).join("\n");
|
|
1815
|
+
} else banner.style.display = "none";
|
|
1816
|
+
}
|
|
1817
|
+
function bumpStats(f, dSeen, dRev, w) {
|
|
1818
|
+
f.seen = Math.max(0, Math.min(f.blocks, f.seen + dSeen));
|
|
1819
|
+
f.reviewed = Math.max(0, Math.min(f.blocks, f.reviewed + dRev));
|
|
1820
|
+
f.seen_w = Math.max(0, Math.min(f.weight, f.seen_w + dSeen * w));
|
|
1821
|
+
f.reviewed_w = Math.max(0, Math.min(f.weight, f.reviewed_w + dRev * w));
|
|
1822
|
+
meta.stats.seen = Math.max(0, meta.stats.seen + dSeen);
|
|
1823
|
+
meta.stats.reviewed = Math.max(0, meta.stats.reviewed + dRev);
|
|
1824
|
+
meta.stats.seen_w = Math.max(0, meta.stats.seen_w + dSeen * w);
|
|
1825
|
+
meta.stats.reviewed_w = Math.max(0, meta.stats.reviewed_w + dRev * w);
|
|
1826
|
+
renderSidebar();
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function loadMeta() {
|
|
1830
|
+
meta = await api("GET", "/api/files");
|
|
1831
|
+
$("btn-hide").style.opacity = hideDone ? 1 : .55;
|
|
1832
|
+
renderSidebar();
|
|
1833
|
+
}
|
|
1834
|
+
async function openFile(path, focusLine) {
|
|
1835
|
+
file = await api("GET", "/api/file?path=" + encodeURIComponent(path));
|
|
1836
|
+
curPath = path;
|
|
1837
|
+
selAnchor = selEnd = null; dwell.clear();
|
|
1838
|
+
clearRows();
|
|
1839
|
+
$("empty-msg")?.remove();
|
|
1840
|
+
const rowsOf = new Map();
|
|
1841
|
+
file.lines.forEach((l, i) => {
|
|
1842
|
+
if (l.h) { if (!rowsOf.has(l.h)) rowsOf.set(l.h, [i, i]); else rowsOf.get(l.h)[1] = i; }
|
|
1843
|
+
});
|
|
1844
|
+
blocks = file.blocks.map(b => {
|
|
1845
|
+
const r = rowsOf.get(b.hunk_id) || [0, Math.max(0, file.lines.length - 1)];
|
|
1846
|
+
return Object.assign({}, b, { l0: r[0], l1: r[1] });
|
|
1847
|
+
}).sort((a, b) => a.l0 - b.l0);
|
|
1848
|
+
buildView();
|
|
1849
|
+
computeBlockRanges();
|
|
1850
|
+
focusIdx = blocks.findIndex(b => !b.reviewed);
|
|
1851
|
+
if (focusIdx < 0) focusIdx = blocks.length ? 0 : -1;
|
|
1852
|
+
|
|
1853
|
+
$("tb-path").textContent = (file.old_path ? file.old_path + " → " : "") + path;
|
|
1854
|
+
$("tb-info").textContent = `${file.total_lines} lines · ${file.lang}`
|
|
1855
|
+
+ (file.skip_reason ? ` · plain (${file.skip_reason})` : "")
|
|
1856
|
+
+ (file.status !== "modified" ? ` · ${file.status}` : "");
|
|
1857
|
+
measureMetrics();
|
|
1858
|
+
computeOffsets();
|
|
1859
|
+
wtRecompute();
|
|
1860
|
+
runFileSearch();
|
|
1861
|
+
pane.scrollTop = focusLine
|
|
1862
|
+
? Math.max(0, offsets[viewOf[Math.min(focusLine - 1, file.lines.length - 1)]]
|
|
1863
|
+
- pane.clientHeight / 3)
|
|
1864
|
+
: 0;
|
|
1865
|
+
renderSidebar();
|
|
1866
|
+
renderRows(); drawMinimap();
|
|
1867
|
+
updateHash();
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function buildView() {
|
|
1871
|
+
const n = file.lines.length;
|
|
1872
|
+
viewRows = [];
|
|
1873
|
+
viewOf = new Array(n).fill(0);
|
|
1874
|
+
if (!split) {
|
|
1875
|
+
for (let i = 0; i < n; i++) { viewOf[i] = i; viewRows.push([i, null]); }
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
let dels = [], adds = [];
|
|
1879
|
+
const flush = () => {
|
|
1880
|
+
for (let k = 0; k < Math.max(dels.length, adds.length); k++) {
|
|
1881
|
+
const L = k < dels.length ? dels[k] : null;
|
|
1882
|
+
const R = k < adds.length ? adds[k] : null;
|
|
1883
|
+
if (L !== null) viewOf[L] = viewRows.length;
|
|
1884
|
+
if (R !== null) viewOf[R] = viewRows.length;
|
|
1885
|
+
viewRows.push([L, R]);
|
|
1886
|
+
}
|
|
1887
|
+
dels = []; adds = [];
|
|
1888
|
+
};
|
|
1889
|
+
for (let i = 0; i < n; i++) {
|
|
1890
|
+
const t = file.lines[i].type;
|
|
1891
|
+
if (t === "del") dels.push(i);
|
|
1892
|
+
else if (t === "add") adds.push(i);
|
|
1893
|
+
else { flush(); viewOf[i] = viewRows.length; viewRows.push([i, i]); }
|
|
1894
|
+
}
|
|
1895
|
+
flush();
|
|
1896
|
+
}
|
|
1897
|
+
function computeBlockRanges() {
|
|
1898
|
+
for (const b of blocks) {
|
|
1899
|
+
let lo = Infinity, hi = -Infinity;
|
|
1900
|
+
for (let i = b.l0; i <= b.l1 && i < viewOf.length; i++) {
|
|
1901
|
+
const v = viewOf[i];
|
|
1902
|
+
if (v < lo) lo = v;
|
|
1903
|
+
if (v > hi) hi = v;
|
|
1904
|
+
}
|
|
1905
|
+
b.r0 = lo === Infinity ? 0 : lo;
|
|
1906
|
+
b.r1 = hi === -Infinity ? 0 : hi;
|
|
1907
|
+
}
|
|
1908
|
+
blockByRow = new Array(viewRows.length).fill(-1);
|
|
1909
|
+
blocks.forEach((b, i) => { for (let r = b.r0; r <= b.r1; r++) blockByRow[r] = i; });
|
|
1910
|
+
}
|
|
1911
|
+
function viewLines(v0, v1) {
|
|
1912
|
+
const s = new Set();
|
|
1913
|
+
for (let v = Math.max(0, v0); v <= Math.min(v1, viewRows.length - 1); v++)
|
|
1914
|
+
for (const idx of viewRows[v]) if (idx !== null) s.add(idx);
|
|
1915
|
+
return [...s].sort((a, b) => a - b);
|
|
1916
|
+
}
|
|
1917
|
+
function firstLineOf(v) {
|
|
1918
|
+
const vr = viewRows[v];
|
|
1919
|
+
return vr ? (vr[0] ?? vr[1] ?? 0) : 0;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
let offsets = [0];
|
|
1923
|
+
let charW = 8, padPx = 114, splitPadPx = 150;
|
|
1924
|
+
function measureMetrics() {
|
|
1925
|
+
const probe = document.createElement("div");
|
|
1926
|
+
probe.className = "row";
|
|
1927
|
+
probe.style.visibility = "hidden";
|
|
1928
|
+
probe.innerHTML = '<span class="ln old">0</span><span class="ln new">0</span>'
|
|
1929
|
+
+ '<span class="chk"> </span><span class="sign"> </span>'
|
|
1930
|
+
+ '<pre class="content">' + "0".repeat(200) + '</pre>';
|
|
1931
|
+
spacer.append(probe);
|
|
1932
|
+
const content = probe.querySelector(".content");
|
|
1933
|
+
content.style.whiteSpace = "pre";
|
|
1934
|
+
const range = document.createRange();
|
|
1935
|
+
range.selectNodeContents(content);
|
|
1936
|
+
charW = range.getBoundingClientRect().width / 200 || 7.5;
|
|
1937
|
+
content.style.whiteSpace = "";
|
|
1938
|
+
padPx = probe.getBoundingClientRect().width - content.getBoundingClientRect().width;
|
|
1939
|
+
probe.remove();
|
|
1940
|
+
|
|
1941
|
+
const sp = document.createElement("div");
|
|
1942
|
+
sp.className = "row split";
|
|
1943
|
+
sp.style.visibility = "hidden";
|
|
1944
|
+
sp.innerHTML = '<span class="chk"> </span>'
|
|
1945
|
+
+ '<div class="half left"><span class="ln old">0</span>'
|
|
1946
|
+
+ '<span class="sign"> </span><pre class="content">0</pre></div>'
|
|
1947
|
+
+ '<div class="half right"><span class="ln new">0</span>'
|
|
1948
|
+
+ '<span class="sign"> </span><pre class="content">0</pre></div>';
|
|
1949
|
+
spacer.append(sp);
|
|
1950
|
+
splitPadPx = sp.getBoundingClientRect().width
|
|
1951
|
+
- 2 * sp.querySelector(".half.left .content").getBoundingClientRect().width;
|
|
1952
|
+
sp.remove();
|
|
1953
|
+
}
|
|
1954
|
+
function displayCols(text) {
|
|
1955
|
+
let col = 0;
|
|
1956
|
+
for (let i = 0; i < text.length; i++) {
|
|
1957
|
+
const c = text.charCodeAt(i);
|
|
1958
|
+
if (c === 9) col += 4 - (col % 4);
|
|
1959
|
+
else if (c >= 0x2e80 && c <= 0xa4cf || c >= 0xac00 && c <= 0xd7a3
|
|
1960
|
+
|| c >= 0xf900 && c <= 0xfaff || c >= 0xff00 && c <= 0xff60)
|
|
1961
|
+
col += 2;
|
|
1962
|
+
else col += 1;
|
|
1963
|
+
}
|
|
1964
|
+
return col;
|
|
1965
|
+
}
|
|
1966
|
+
function rowText(l) { return l.t ? l.t.map(t => t[1]).join("") : (l.text ?? ""); }
|
|
1967
|
+
function computeOffsets() {
|
|
1968
|
+
if (!file) return;
|
|
1969
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
1970
|
+
const paneW = pane.clientWidth;
|
|
1971
|
+
const avail = split
|
|
1972
|
+
? Math.max(1, (paneW - splitPadPx) / 2)
|
|
1973
|
+
: Math.max(1, paneW - padPx);
|
|
1974
|
+
const cols = Math.max(1, Math.floor((avail - 1) / charW));
|
|
1975
|
+
const n = viewRows.length;
|
|
1976
|
+
offsets = new Array(n + 1);
|
|
1977
|
+
offsets[0] = 0;
|
|
1978
|
+
for (let v = 0; v < n; v++) {
|
|
1979
|
+
let h = 1;
|
|
1980
|
+
for (const idx of viewRows[v]) {
|
|
1981
|
+
if (idx === null) continue;
|
|
1982
|
+
const c = displayCols(rowText(file.lines[idx]));
|
|
1983
|
+
h = Math.max(h, Math.ceil(c / cols));
|
|
1984
|
+
}
|
|
1985
|
+
offsets[v + 1] = offsets[v] + h * LH;
|
|
1986
|
+
}
|
|
1987
|
+
spacer.style.height = (offsets[n] + 40) + "px";
|
|
1988
|
+
for (const b of blocks) b.cov = undefined;
|
|
1989
|
+
if (pane.clientWidth === paneW) break;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function totalHeight() { return offsets[offsets.length - 1] + 40; }
|
|
1993
|
+
function findRow(y) {
|
|
1994
|
+
let lo = 0, hi = offsets.length - 2;
|
|
1995
|
+
while (lo < hi) {
|
|
1996
|
+
const mid = (lo + hi + 1) >> 1;
|
|
1997
|
+
if (offsets[mid] <= y) lo = mid; else hi = mid - 1;
|
|
1998
|
+
}
|
|
1999
|
+
return lo;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
let rowEls = new Map();
|
|
2003
|
+
function renderRows() {
|
|
2004
|
+
if (!file || !viewRows.length) return;
|
|
2005
|
+
const first = Math.max(0, findRow(pane.scrollTop) - 30);
|
|
2006
|
+
const last = Math.min(viewRows.length - 1,
|
|
2007
|
+
findRow(pane.scrollTop + pane.clientHeight) + 30);
|
|
2008
|
+
for (const [i, el] of rowEls) {
|
|
2009
|
+
if (i < first || i > last) { el.remove(); rowEls.delete(i); }
|
|
2010
|
+
}
|
|
2011
|
+
const focusB = focusIdx >= 0 ? blocks[focusIdx] : null;
|
|
2012
|
+
const s0 = selAnchor === null ? -1 : Math.min(selAnchor, selEnd);
|
|
2013
|
+
const s1 = selAnchor === null ? -1 : Math.max(selAnchor, selEnd);
|
|
2014
|
+
for (let i = first; i <= last; i++) {
|
|
2015
|
+
let el = rowEls.get(i);
|
|
2016
|
+
if (!el) { el = buildRow(i); rowEls.set(i, el); spacer.append(el); }
|
|
2017
|
+
el.style.top = offsets[i] + "px";
|
|
2018
|
+
el.style.height = (offsets[i + 1] - offsets[i]) + "px";
|
|
2019
|
+
el.classList.toggle("focused-block",
|
|
2020
|
+
!!focusB && i >= focusB.r0 && i <= focusB.r1);
|
|
2021
|
+
el.classList.toggle("selected", i >= s0 && i <= s1);
|
|
2022
|
+
el.classList.toggle("wt-hi", !!wtHi && i >= wtHi.r0 && i <= wtHi.r1);
|
|
2023
|
+
el.classList.toggle("wt-anchor", wtAnchors.has(i));
|
|
2024
|
+
const bi = blockByRow[i];
|
|
2025
|
+
const chk = el.querySelector(".chk");
|
|
2026
|
+
if (chk && chk.dataset.active && bi >= 0) {
|
|
2027
|
+
chk.classList.toggle("reviewed", !!blocks[bi].reviewed);
|
|
2028
|
+
chk.classList.toggle("seen", !!blocks[bi].seen && !blocks[bi].reviewed);
|
|
2029
|
+
chk.textContent = blocks[bi].reviewed ? "☑" : "☐";
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
function appendTok(parent, cls, text, hit) {
|
|
2034
|
+
let node = parent;
|
|
2035
|
+
if (hit) {
|
|
2036
|
+
node = document.createElement("mark");
|
|
2037
|
+
if (hit === sMatches[sIdx]) node.className = "cur";
|
|
2038
|
+
parent.append(node);
|
|
2039
|
+
}
|
|
2040
|
+
if (cls) {
|
|
2041
|
+
const sp = document.createElement("span");
|
|
2042
|
+
sp.className = cls; sp.textContent = text;
|
|
2043
|
+
node.append(sp);
|
|
2044
|
+
} else node.append(text);
|
|
2045
|
+
}
|
|
2046
|
+
function contentEl(l, idx) {
|
|
2047
|
+
const content = document.createElement("pre");
|
|
2048
|
+
content.className = "content hl";
|
|
2049
|
+
if (!l) return content;
|
|
2050
|
+
const toks = l.t || [["", l.text ?? ""]];
|
|
2051
|
+
const hits = idx == null ? null : sByLine.get(idx);
|
|
2052
|
+
if (!hits) {
|
|
2053
|
+
for (const [cls, text] of toks) appendTok(content, cls, text, null);
|
|
2054
|
+
return content;
|
|
2055
|
+
}
|
|
2056
|
+
let pos = 0, hi = 0;
|
|
2057
|
+
for (const [cls, text] of toks) {
|
|
2058
|
+
let off = 0;
|
|
2059
|
+
while (off < text.length) {
|
|
2060
|
+
while (hi < hits.length && hits[hi].e <= pos + off) hi++;
|
|
2061
|
+
const h = hits[hi];
|
|
2062
|
+
let end, inside = null;
|
|
2063
|
+
if (!h || h.s >= pos + text.length) end = text.length;
|
|
2064
|
+
else if (h.s > pos + off) end = h.s - pos;
|
|
2065
|
+
else { end = Math.min(text.length, h.e - pos); inside = h; }
|
|
2066
|
+
appendTok(content, cls, text.slice(off, end), inside);
|
|
2067
|
+
off = end;
|
|
2068
|
+
}
|
|
2069
|
+
pos += text.length;
|
|
2070
|
+
}
|
|
2071
|
+
return content;
|
|
2072
|
+
}
|
|
2073
|
+
function chkEl(v) {
|
|
2074
|
+
const chk = document.createElement("span");
|
|
2075
|
+
chk.className = "chk";
|
|
2076
|
+
const bi = blockByRow[v];
|
|
2077
|
+
if (bi >= 0 && v === blocks[bi].r0) {
|
|
2078
|
+
chk.dataset.active = "1";
|
|
2079
|
+
chk.title = "toggle reviewed (R)";
|
|
2080
|
+
chk.onclick = () => toggleReviewed(bi);
|
|
2081
|
+
} else chk.textContent = " ";
|
|
2082
|
+
return chk;
|
|
2083
|
+
}
|
|
2084
|
+
function buildRow(v) {
|
|
2085
|
+
return split ? buildSplitRow(v) : buildUnifiedRow(v);
|
|
2086
|
+
}
|
|
2087
|
+
function buildUnifiedRow(v) {
|
|
2088
|
+
const l = file.lines[viewRows[v][0]];
|
|
2089
|
+
const el = document.createElement("div");
|
|
2090
|
+
el.className = "row " + l.type;
|
|
2091
|
+
const lnOld = document.createElement("span");
|
|
2092
|
+
lnOld.className = "ln old"; lnOld.textContent = l.old ?? "";
|
|
2093
|
+
const lnNew = document.createElement("span");
|
|
2094
|
+
lnNew.className = "ln new"; lnNew.textContent = l.new ?? "";
|
|
2095
|
+
lnNew.dataset.row = lnOld.dataset.row = v;
|
|
2096
|
+
const sign = document.createElement("span");
|
|
2097
|
+
sign.className = "sign";
|
|
2098
|
+
sign.textContent = l.type === "add" ? "+" : l.type === "del" ? "−" : " ";
|
|
2099
|
+
el.append(lnOld, lnNew, chkEl(v), sign, contentEl(l, viewRows[v][0]));
|
|
2100
|
+
return el;
|
|
2101
|
+
}
|
|
2102
|
+
function buildSplitRow(v) {
|
|
2103
|
+
const el = document.createElement("div");
|
|
2104
|
+
el.className = "row split";
|
|
2105
|
+
el.append(chkEl(v), halfEl(v, "left"), halfEl(v, "right"));
|
|
2106
|
+
return el;
|
|
2107
|
+
}
|
|
2108
|
+
function halfEl(v, side) {
|
|
2109
|
+
const left = side === "left";
|
|
2110
|
+
const idx = viewRows[v][left ? 0 : 1];
|
|
2111
|
+
const l = idx === null ? null : file.lines[idx];
|
|
2112
|
+
const h = document.createElement("div");
|
|
2113
|
+
h.className = "half " + side + " "
|
|
2114
|
+
+ (l ? (l.type === "context" ? "ctx" : l.type) : "empty");
|
|
2115
|
+
const ln = document.createElement("span");
|
|
2116
|
+
ln.className = "ln " + (left ? "old" : "new");
|
|
2117
|
+
ln.textContent = l ? ((left ? l.old : l.new) ?? "") : "";
|
|
2118
|
+
if (l) ln.dataset.row = v;
|
|
2119
|
+
const sign = document.createElement("span");
|
|
2120
|
+
sign.className = "sign";
|
|
2121
|
+
sign.textContent = !l || l.type === "context" ? " " : l.type === "add" ? "+" : "−";
|
|
2122
|
+
h.append(ln, sign, contentEl(l, idx));
|
|
2123
|
+
return h;
|
|
2124
|
+
}
|
|
2125
|
+
function clearRows() { for (const el of rowEls.values()) el.remove(); rowEls.clear(); }
|
|
2126
|
+
|
|
2127
|
+
function mmColor(kind) {
|
|
2128
|
+
const cs = getComputedStyle(document.documentElement);
|
|
2129
|
+
return (kind === "add" ? cs.getPropertyValue("--add-sign")
|
|
2130
|
+
: kind === "del" ? cs.getPropertyValue("--del-sign")
|
|
2131
|
+
: cs.getPropertyValue("--mixed")).trim();
|
|
2132
|
+
}
|
|
2133
|
+
function drawMinimap() {
|
|
2134
|
+
const dpr = devicePixelRatio || 1;
|
|
2135
|
+
const w = mm.clientWidth, h = mm.clientHeight;
|
|
2136
|
+
if (!w || !h) return;
|
|
2137
|
+
if (mm.width !== Math.round(w * dpr)) { mm.width = Math.round(w * dpr); mm.height = Math.round(h * dpr); }
|
|
2138
|
+
const ctx = mm.getContext("2d");
|
|
2139
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2140
|
+
ctx.clearRect(0, 0, w, h);
|
|
2141
|
+
if (!file || !file.lines.length) return;
|
|
2142
|
+
const T = totalHeight();
|
|
2143
|
+
for (const b of blocks) {
|
|
2144
|
+
const y = offsets[b.r0] / T * h;
|
|
2145
|
+
const bh = Math.max(3, (offsets[b.r1 + 1] - offsets[b.r0]) / T * h);
|
|
2146
|
+
const col = mmColor(b.kind);
|
|
2147
|
+
if (b.reviewed) {
|
|
2148
|
+
ctx.strokeStyle = col; ctx.lineWidth = 1;
|
|
2149
|
+
ctx.strokeRect(2.5, y + .5, w - 5, Math.max(2, bh - 1));
|
|
2150
|
+
} else {
|
|
2151
|
+
ctx.globalAlpha = b.seen ? .45 : 1;
|
|
2152
|
+
ctx.fillStyle = col;
|
|
2153
|
+
ctx.fillRect(2, y, w - 4, bh);
|
|
2154
|
+
ctx.globalAlpha = 1;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (sOpen && sMatches.length) {
|
|
2158
|
+
ctx.fillStyle = getComputedStyle(document.documentElement)
|
|
2159
|
+
.getPropertyValue("--mixed").trim();
|
|
2160
|
+
const drawn = new Set();
|
|
2161
|
+
sMatches.forEach((m, k) => {
|
|
2162
|
+
const y = Math.round(offsets[viewOf[m.i]] / T * h);
|
|
2163
|
+
if (k !== sIdx) { if (drawn.has(y)) return; drawn.add(y); }
|
|
2164
|
+
ctx.globalAlpha = k === sIdx ? 1 : .55;
|
|
2165
|
+
ctx.fillRect(0, y, k === sIdx ? w : 4, k === sIdx ? 2 : 1.5);
|
|
2166
|
+
});
|
|
2167
|
+
ctx.globalAlpha = 1;
|
|
2168
|
+
}
|
|
2169
|
+
if (wtHi || wtAnchors.size) {
|
|
2170
|
+
const accent = getComputedStyle(document.documentElement)
|
|
2171
|
+
.getPropertyValue("--accent").trim();
|
|
2172
|
+
if (wtHi) {
|
|
2173
|
+
const y = offsets[wtHi.r0] / T * h;
|
|
2174
|
+
const bh = Math.max(4, (offsets[wtHi.r1 + 1] - offsets[wtHi.r0]) / T * h);
|
|
2175
|
+
ctx.strokeStyle = accent; ctx.lineWidth = 2;
|
|
2176
|
+
ctx.beginPath();
|
|
2177
|
+
ctx.moveTo(4, y + 1); ctx.lineTo(1, y + 1);
|
|
2178
|
+
ctx.lineTo(1, y + bh - 1); ctx.lineTo(4, y + bh - 1);
|
|
2179
|
+
ctx.stroke();
|
|
2180
|
+
}
|
|
2181
|
+
ctx.fillStyle = accent;
|
|
2182
|
+
for (const v of wtAnchors) {
|
|
2183
|
+
ctx.globalAlpha = .6;
|
|
2184
|
+
ctx.fillRect(w - 5, offsets[v] / T * h, 4, 2);
|
|
2185
|
+
ctx.globalAlpha = 1;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const y0 = pane.scrollTop / T * h;
|
|
2189
|
+
const y1 = (pane.scrollTop + pane.clientHeight) / T * h;
|
|
2190
|
+
ctx.fillStyle = getComputedStyle(document.documentElement)
|
|
2191
|
+
.getPropertyValue("--fg").trim();
|
|
2192
|
+
ctx.globalAlpha = .12;
|
|
2193
|
+
ctx.fillRect(0, y0, w, Math.max(4, y1 - y0));
|
|
2194
|
+
ctx.globalAlpha = 1;
|
|
2195
|
+
}
|
|
2196
|
+
mm.addEventListener("click", e => {
|
|
2197
|
+
if (!file) return;
|
|
2198
|
+
const h = mm.clientHeight, T = totalHeight();
|
|
2199
|
+
const y = e.offsetY;
|
|
2200
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2201
|
+
const b = blocks[i];
|
|
2202
|
+
const by = offsets[b.r0] / T * h;
|
|
2203
|
+
const bh = Math.max(3, (offsets[b.r1 + 1] - offsets[b.r0]) / T * h);
|
|
2204
|
+
if (y >= by - 2 && y <= by + bh + 2) { focusBlock(i); return; }
|
|
2205
|
+
}
|
|
2206
|
+
pane.scrollTop = y / h * T - pane.clientHeight / 2;
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
function blockWeight(b) { return (b.added + b.removed) || 1; }
|
|
2210
|
+
function setLocal(b, seen, reviewed) {
|
|
2211
|
+
const f = meta.files.find(x => x.path === curPath);
|
|
2212
|
+
const wasSeen = b.seen && !b.reviewed, wasRev = !!b.reviewed;
|
|
2213
|
+
b.seen = seen; b.reviewed = reviewed;
|
|
2214
|
+
const isSeen = seen && !reviewed, isRev = reviewed;
|
|
2215
|
+
if (f) bumpStats(f, (isSeen ? 1 : 0) - (wasSeen ? 1 : 0),
|
|
2216
|
+
(isRev ? 1 : 0) - (wasRev ? 1 : 0), blockWeight(b));
|
|
2217
|
+
renderRows(); drawMinimap();
|
|
2218
|
+
}
|
|
2219
|
+
function toggleReviewed(bi) {
|
|
2220
|
+
const b = blocks[bi];
|
|
2221
|
+
if (!b) return;
|
|
2222
|
+
if (b.reviewed) {
|
|
2223
|
+
api("DELETE", `/api/reviewed?path=${encodeURIComponent(curPath)}&hunk_id=${encodeURIComponent(b.hunk_id)}`);
|
|
2224
|
+
setLocal(b, true, false);
|
|
2225
|
+
} else {
|
|
2226
|
+
api("POST", "/api/reviewed", { blocks: [{ path: curPath, hunk_id: b.hunk_id }] });
|
|
2227
|
+
setLocal(b, true, true);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
function toggleFileReviewed(f) {
|
|
2231
|
+
const done = f.reviewed >= f.blocks;
|
|
2232
|
+
if (done) {
|
|
2233
|
+
api("DELETE", "/api/reviewed?file=" + encodeURIComponent(f.path));
|
|
2234
|
+
if (f.path === curPath) blocks.forEach(b => { b.reviewed = false; b.seen = true; });
|
|
2235
|
+
meta.stats.reviewed -= f.reviewed; meta.stats.seen += f.reviewed;
|
|
2236
|
+
meta.stats.reviewed_w -= f.reviewed_w; meta.stats.seen_w += f.reviewed_w;
|
|
2237
|
+
f.seen = f.blocks; f.reviewed = 0;
|
|
2238
|
+
f.seen_w = f.weight; f.reviewed_w = 0;
|
|
2239
|
+
} else {
|
|
2240
|
+
api("POST", "/api/reviewed", { file: f.path });
|
|
2241
|
+
if (f.path === curPath) blocks.forEach(b => { b.reviewed = true; b.seen = true; });
|
|
2242
|
+
meta.stats.reviewed += f.blocks - f.reviewed;
|
|
2243
|
+
meta.stats.seen -= f.seen;
|
|
2244
|
+
meta.stats.reviewed_w += f.weight - f.reviewed_w;
|
|
2245
|
+
meta.stats.seen_w -= f.seen_w;
|
|
2246
|
+
f.reviewed = f.blocks; f.seen = 0;
|
|
2247
|
+
f.reviewed_w = f.weight; f.seen_w = 0;
|
|
2248
|
+
}
|
|
2249
|
+
renderSidebar(); renderRows(); drawMinimap();
|
|
2250
|
+
}
|
|
2251
|
+
function reviewSeenInFile() { // V
|
|
2252
|
+
const targets = blocks.filter(b => b.seen && !b.reviewed);
|
|
2253
|
+
if (!targets.length) { toast("nothing seen-but-unreviewed"); return; }
|
|
2254
|
+
api("POST", "/api/reviewed",
|
|
2255
|
+
{ blocks: targets.map(b => ({ path: curPath, hunk_id: b.hunk_id })) });
|
|
2256
|
+
targets.forEach(b => setLocal(b, true, true));
|
|
2257
|
+
toast(`${targets.length} block${targets.length > 1 ? "s" : ""} reviewed`);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
function evalSeen() {
|
|
2261
|
+
if (!file || !meta.auto_seen || !document.hasFocus()) { dwell.clear(); return; }
|
|
2262
|
+
const top = pane.scrollTop, bot = pane.scrollTop + pane.clientHeight;
|
|
2263
|
+
const now = performance.now();
|
|
2264
|
+
const atBottom = pane.scrollTop + pane.clientHeight >= totalHeight() - 2;
|
|
2265
|
+
blocks.forEach((b, i) => {
|
|
2266
|
+
if (b.seen || b.reviewed) return;
|
|
2267
|
+
const y0 = offsets[b.r0], y1 = offsets[b.r1 + 1];
|
|
2268
|
+
if (y1 < top) { markSeen(b); return; }
|
|
2269
|
+
if (y0 > bot) { dwell.delete(i); return; }
|
|
2270
|
+
if (b.cov === undefined) b.cov = y0;
|
|
2271
|
+
if (top <= b.cov) b.cov = Math.max(b.cov, Math.min(bot, y1));
|
|
2272
|
+
if (b.cov >= y1 || atBottom) {
|
|
2273
|
+
if (!dwell.has(i)) dwell.set(i, now);
|
|
2274
|
+
else if (now - dwell.get(i) >= 500) markSeen(b);
|
|
2275
|
+
} else dwell.delete(i);
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
function markSeen(b) {
|
|
2279
|
+
seenQueue.set(curPath + "|" + b.hunk_id, { path: curPath, hunk_id: b.hunk_id });
|
|
2280
|
+
setLocal(b, true, false);
|
|
2281
|
+
}
|
|
2282
|
+
function flushSeen(beacon) {
|
|
2283
|
+
if (!seenQueue.size) return;
|
|
2284
|
+
const payload = JSON.stringify({ blocks: [...seenQueue.values()] });
|
|
2285
|
+
seenQueue.clear();
|
|
2286
|
+
if (beacon && navigator.sendBeacon) {
|
|
2287
|
+
navigator.sendBeacon("/api/seen", new Blob([payload], { type: "application/json" }));
|
|
2288
|
+
} else {
|
|
2289
|
+
fetch("/api/seen", {
|
|
2290
|
+
method: "POST", body: payload,
|
|
2291
|
+
headers: { "content-type": "application/json" }
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
setInterval(() => { evalSeen(); flushSeen(false); wtPoll(); }, 2000);
|
|
2296
|
+
document.addEventListener("visibilitychange", () => {
|
|
2297
|
+
if (document.visibilityState === "hidden") flushSeen(true);
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
function focusBlock(bi) {
|
|
2301
|
+
if (bi < 0 || bi >= blocks.length) return;
|
|
2302
|
+
focusIdx = bi;
|
|
2303
|
+
const b = blocks[bi];
|
|
2304
|
+
pane.scrollTop = Math.max(0, offsets[b.r0] - pane.clientHeight / 3);
|
|
2305
|
+
renderRows(); drawMinimap();
|
|
2306
|
+
}
|
|
2307
|
+
function nextBlock(dir) {
|
|
2308
|
+
if (!blocks.length) return;
|
|
2309
|
+
focusBlock(Math.min(blocks.length - 1, Math.max(0, focusIdx + dir)));
|
|
2310
|
+
}
|
|
2311
|
+
function fileIndex() { return meta.files.findIndex(f => f.path === curPath); }
|
|
2312
|
+
function visibleFiles() {
|
|
2313
|
+
return meta.files.filter(f => !(hideDone && f.reviewed >= f.blocks));
|
|
2314
|
+
}
|
|
2315
|
+
function nextFile(dir) {
|
|
2316
|
+
const vis = visibleFiles();
|
|
2317
|
+
if (!vis.length) return;
|
|
2318
|
+
const i = vis.findIndex(f => f.path === curPath);
|
|
2319
|
+
const j = i < 0 ? (dir > 0 ? 0 : vis.length - 1)
|
|
2320
|
+
: Math.min(vis.length - 1, Math.max(0, i + dir));
|
|
2321
|
+
if (vis[j].path !== curPath) openFile(vis[j].path);
|
|
2322
|
+
}
|
|
2323
|
+
async function nextUnreviewed(dir) {
|
|
2324
|
+
if (file) {
|
|
2325
|
+
const order = blocks.map((b, i) => i).filter(i => !blocks[i].reviewed &&
|
|
2326
|
+
(dir > 0 ? i > focusIdx : i < focusIdx));
|
|
2327
|
+
if (order.length) { focusBlock(dir > 0 ? order[0] : order[order.length - 1]); return; }
|
|
2328
|
+
}
|
|
2329
|
+
const files = meta.files, start = Math.max(0, fileIndex());
|
|
2330
|
+
for (let k = 1; k <= files.length; k++) {
|
|
2331
|
+
const j = ((start + dir * k) % files.length + files.length) % files.length;
|
|
2332
|
+
if (files[j].reviewed < files[j].blocks) {
|
|
2333
|
+
await openFile(files[j].path);
|
|
2334
|
+
const cand = blocks.map((b, i) => i).filter(i => !blocks[i].reviewed);
|
|
2335
|
+
if (cand.length) focusBlock(dir > 0 ? cand[0] : cand[cand.length - 1]);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
toast("everything reviewed 🎉");
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
function refRangeFromRows(r0, r1) {
|
|
2343
|
+
const idxs = viewLines(r0, r1);
|
|
2344
|
+
const news = idxs.map(i => file.lines[i].new).filter(Boolean);
|
|
2345
|
+
if (news.length) return { a: news[0], b: news[news.length - 1], deleted: false };
|
|
2346
|
+
let lastNewBefore = 0;
|
|
2347
|
+
for (let i = 0; i < (idxs.length ? idxs[0] : 0); i++) {
|
|
2348
|
+
const l = file.lines[i];
|
|
2349
|
+
if (l.new) lastNewBefore = l.new;
|
|
2350
|
+
}
|
|
2351
|
+
return { a: Math.max(1, lastNewBefore), b: Math.max(1, lastNewBefore), deleted: true };
|
|
2352
|
+
}
|
|
2353
|
+
function refString(r) { return curPath + ":" + (r.a === r.b ? r.a : r.a + "-" + r.b); }
|
|
2354
|
+
function copyGutterSelection() {
|
|
2355
|
+
const r0 = Math.min(selAnchor, selEnd), r1 = Math.max(selAnchor, selEnd);
|
|
2356
|
+
const r = refRangeFromRows(r0, r1);
|
|
2357
|
+
copyText(refString(r), refString(r) + (r.deleted ? " (deleted lines)" : ""));
|
|
2358
|
+
}
|
|
2359
|
+
function copyRefWithContent() {
|
|
2360
|
+
let r0, r1;
|
|
2361
|
+
if (selAnchor !== null) { r0 = Math.min(selAnchor, selEnd); r1 = Math.max(selAnchor, selEnd); }
|
|
2362
|
+
else if (focusIdx >= 0) { r0 = blocks[focusIdx].r0; r1 = blocks[focusIdx].r1; }
|
|
2363
|
+
else { toast("nothing selected"); return; }
|
|
2364
|
+
const r = refRangeFromRows(r0, r1);
|
|
2365
|
+
const idxs = viewLines(r0, r1);
|
|
2366
|
+
const body = idxs.map(i => rowText(file.lines[i])).join("\n");
|
|
2367
|
+
const text = refString(r) + (r.deleted ? " (deleted lines)" : "")
|
|
2368
|
+
+ "\n```" + (file.lang || "") + "\n" + body + "\n```\n";
|
|
2369
|
+
copyText(text, "ref + " + idxs.length + " lines");
|
|
2370
|
+
}
|
|
2371
|
+
function copyUnreviewedRefs() {
|
|
2372
|
+
const un = blocks.filter(b => !b.reviewed);
|
|
2373
|
+
if (!un.length) { toast("no unreviewed blocks"); return; }
|
|
2374
|
+
const refs = un.map(b => refString(refRangeFromRows(b.r0, b.r1)));
|
|
2375
|
+
copyText(refs.join("\n"), un.length + " refs");
|
|
2376
|
+
}
|
|
2377
|
+
let dragging = false;
|
|
2378
|
+
pane.addEventListener("mousedown", e => {
|
|
2379
|
+
const ln = e.target.closest(".ln");
|
|
2380
|
+
if (!ln) return;
|
|
2381
|
+
e.preventDefault();
|
|
2382
|
+
const row = +ln.dataset.row;
|
|
2383
|
+
if (wtOn && !e.shiftKey && wtAnchors.has(row)) {
|
|
2384
|
+
const ar = wt.steps[wtStep].also.find(a =>
|
|
2385
|
+
a.path === curPath && wtAnchors.has(row) && viewOf[wtLineRange(a)[0]] === row);
|
|
2386
|
+
if (ar) { wtJumpRef(ar); return; }
|
|
2387
|
+
}
|
|
2388
|
+
if (e.shiftKey && selAnchor !== null) selEnd = row;
|
|
2389
|
+
else { selAnchor = row; selEnd = row; }
|
|
2390
|
+
dragging = true;
|
|
2391
|
+
renderRows();
|
|
2392
|
+
});
|
|
2393
|
+
pane.addEventListener("mousemove", e => {
|
|
2394
|
+
if (!dragging) return;
|
|
2395
|
+
const ln = e.target.closest(".ln");
|
|
2396
|
+
if (ln) { selEnd = +ln.dataset.row; renderRows(); }
|
|
2397
|
+
});
|
|
2398
|
+
addEventListener("mouseup", () => {
|
|
2399
|
+
if (!dragging) return;
|
|
2400
|
+
dragging = false;
|
|
2401
|
+
copyGutterSelection();
|
|
2402
|
+
});
|
|
2403
|
+
{
|
|
2404
|
+
const sb = $("sidebar"), rz = $("sb-resizer");
|
|
2405
|
+
const saved = parseInt(localStorage.getItem("seendiff.sbw") || "0", 10);
|
|
2406
|
+
if (saved) sb.style.width = saved + "px";
|
|
2407
|
+
let resizing = false;
|
|
2408
|
+
rz.addEventListener("mousedown", e => {
|
|
2409
|
+
e.preventDefault();
|
|
2410
|
+
resizing = true;
|
|
2411
|
+
rz.classList.add("active");
|
|
2412
|
+
document.body.style.cursor = "col-resize";
|
|
2413
|
+
});
|
|
2414
|
+
addEventListener("mousemove", e => {
|
|
2415
|
+
if (!resizing) return;
|
|
2416
|
+
sb.style.width = e.clientX + "px";
|
|
2417
|
+
layoutNotches();
|
|
2418
|
+
});
|
|
2419
|
+
addEventListener("mouseup", () => {
|
|
2420
|
+
if (!resizing) return;
|
|
2421
|
+
resizing = false;
|
|
2422
|
+
rz.classList.remove("active");
|
|
2423
|
+
document.body.style.cursor = "";
|
|
2424
|
+
savePref("seendiff.sbw", sb.getBoundingClientRect().width | 0);
|
|
2425
|
+
if (meta) renderSidebar();
|
|
2426
|
+
if (file) { computeOffsets(); renderRows(); drawMinimap(); }
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function setTheme(t) {
|
|
2431
|
+
document.documentElement.dataset.theme = t;
|
|
2432
|
+
savePref("seendiff.theme", t);
|
|
2433
|
+
$("btn-theme").textContent = t;
|
|
2434
|
+
drawMinimap();
|
|
2435
|
+
}
|
|
2436
|
+
function setFont(f) {
|
|
2437
|
+
document.documentElement.style.setProperty("--mono-font", `'${f}'`);
|
|
2438
|
+
savePref("seendiff.font", f);
|
|
2439
|
+
$("btn-font").textContent = f === "JetBrains Mono" ? "JB Mono" : "Ubuntu Mono";
|
|
2440
|
+
document.fonts.ready.then(() => {
|
|
2441
|
+
if (file) { measureMetrics(); computeOffsets(); renderRows(); drawMinimap(); }
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
$("btn-theme").onclick = () =>
|
|
2445
|
+
setTheme(THEMES[(THEMES.indexOf(document.documentElement.dataset.theme) + 1) % THEMES.length]);
|
|
2446
|
+
$("btn-font").onclick = () => {
|
|
2447
|
+
const cur = localStorage.getItem("seendiff.font") || FONTS[0];
|
|
2448
|
+
setFont(FONTS[(FONTS.indexOf(cur) + 1) % FONTS.length]);
|
|
2449
|
+
};
|
|
2450
|
+
function setSplit(on) {
|
|
2451
|
+
split = on;
|
|
2452
|
+
savePref("seendiff.split", on ? "1" : "0");
|
|
2453
|
+
$("btn-split").textContent = on ? "split" : "unified";
|
|
2454
|
+
if (!file) return;
|
|
2455
|
+
const anchorView = findRow(pane.scrollTop);
|
|
2456
|
+
const anchorLine = firstLineOf(anchorView);
|
|
2457
|
+
const delta = pane.scrollTop - (offsets[anchorView] || 0);
|
|
2458
|
+
selAnchor = selEnd = null;
|
|
2459
|
+
dwell.clear();
|
|
2460
|
+
buildView();
|
|
2461
|
+
computeBlockRanges();
|
|
2462
|
+
wtRecompute();
|
|
2463
|
+
measureMetrics();
|
|
2464
|
+
computeOffsets();
|
|
2465
|
+
clearRows();
|
|
2466
|
+
pane.scrollTop = Math.max(0, (offsets[viewOf[anchorLine]] || 0) + delta);
|
|
2467
|
+
renderRows(); drawMinimap();
|
|
2468
|
+
}
|
|
2469
|
+
$("btn-split").onclick = () => setSplit(!split);
|
|
2470
|
+
|
|
2471
|
+
function askResetAll() {
|
|
2472
|
+
const s = meta.stats;
|
|
2473
|
+
$("confirm-msg").textContent =
|
|
2474
|
+
`Reset review state for this branch? ${s.reviewed} reviewed and `
|
|
2475
|
+
+ `${s.seen} seen marks across ${meta.files.length} files will be cleared.`;
|
|
2476
|
+
$("confirm-overlay").classList.add("show");
|
|
2477
|
+
}
|
|
2478
|
+
async function doResetAll() {
|
|
2479
|
+
$("confirm-overlay").classList.remove("show");
|
|
2480
|
+
await api("DELETE", "/api/state");
|
|
2481
|
+
seenQueue.clear(); dwell.clear();
|
|
2482
|
+
meta.stats.seen = 0; meta.stats.reviewed = 0;
|
|
2483
|
+
meta.stats.seen_w = 0; meta.stats.reviewed_w = 0;
|
|
2484
|
+
meta.files.forEach(f => { f.seen = 0; f.reviewed = 0; f.seen_w = 0; f.reviewed_w = 0; });
|
|
2485
|
+
blocks.forEach(b => { b.seen = false; b.reviewed = false; });
|
|
2486
|
+
renderSidebar(); renderRows(); drawMinimap();
|
|
2487
|
+
toast("review state reset");
|
|
2488
|
+
}
|
|
2489
|
+
$("confirm-yes").onclick = doResetAll;
|
|
2490
|
+
$("confirm-no").onclick = () => $("confirm-overlay").classList.remove("show");
|
|
2491
|
+
$("confirm-overlay").onclick = e => {
|
|
2492
|
+
if (e.target === $("confirm-overlay")) $("confirm-overlay").classList.remove("show");
|
|
2493
|
+
};
|
|
2494
|
+
function toggleHelp(force) {
|
|
2495
|
+
$("help-overlay").classList.toggle("show", force);
|
|
2496
|
+
}
|
|
2497
|
+
$("btn-keys").onclick = () => toggleHelp();
|
|
2498
|
+
$("help-overlay").onclick = e => { if (e.target === $("help-overlay")) toggleHelp(false); };
|
|
2499
|
+
$("btn-hide").onclick = () => {
|
|
2500
|
+
hideDone = !hideDone;
|
|
2501
|
+
savePref("seendiff.hide", hideDone ? "1" : "0");
|
|
2502
|
+
$("btn-hide").style.opacity = hideDone ? 1 : .55;
|
|
2503
|
+
renderSidebar();
|
|
2504
|
+
};
|
|
2505
|
+
|
|
2506
|
+
async function loadWalkthrough() {
|
|
2507
|
+
try {
|
|
2508
|
+
const r = await fetch("/api/walkthrough");
|
|
2509
|
+
wtWatch = r.status !== 404;
|
|
2510
|
+
const d = r.ok ? await r.json() : null;
|
|
2511
|
+
wt = d && !d.empty ? d : null;
|
|
2512
|
+
} catch (e) { wt = null; }
|
|
2513
|
+
$("help-wt").hidden = !wt;
|
|
2514
|
+
}
|
|
2515
|
+
function wtAdopt(nw) {
|
|
2516
|
+
wt = nw;
|
|
2517
|
+
wtVisited = new Set();
|
|
2518
|
+
wtStep = Math.max(0, wtRestoreProgress());
|
|
2519
|
+
$("help-wt").hidden = false;
|
|
2520
|
+
const em = $("empty-msg");
|
|
2521
|
+
if (em) em.remove();
|
|
2522
|
+
renderSidebar();
|
|
2523
|
+
setWalkthrough(true);
|
|
2524
|
+
toast(wt.title);
|
|
2525
|
+
}
|
|
2526
|
+
function wtClear(msg) {
|
|
2527
|
+
wt = null; wtOn = false; wtStep = -1; wtAway = false;
|
|
2528
|
+
wtVisited = new Set(); wtHi = null; wtAnchors = new Set();
|
|
2529
|
+
$("wt-bar").hidden = true;
|
|
2530
|
+
$("help-wt").hidden = true;
|
|
2531
|
+
renderWtOutline();
|
|
2532
|
+
if (file) { computeOffsets(); renderRows(); drawMinimap(); }
|
|
2533
|
+
renderSidebar(); updateHash();
|
|
2534
|
+
if (msg) toast(msg);
|
|
2535
|
+
}
|
|
2536
|
+
function wtStepIdx(id) { return wt ? wt.steps.findIndex(s => s.id === id) : -1; }
|
|
2537
|
+
function wtSeq() {
|
|
2538
|
+
return wt.steps.map((s, i) => i).filter(i => wtDetail || wt.steps[i].depth === 0);
|
|
2539
|
+
}
|
|
2540
|
+
function wtBase(p) { return p.split("/").pop(); }
|
|
2541
|
+
function wtRefLabel(r) {
|
|
2542
|
+
return wtBase(r.path) + ":" + (r.lines[0] === r.lines[1]
|
|
2543
|
+
? r.lines[0] : r.lines[0] + "-" + r.lines[1]);
|
|
2544
|
+
}
|
|
2545
|
+
function wtLineRange(ref) {
|
|
2546
|
+
if (ref.hunk_id) {
|
|
2547
|
+
const b = blocks.find(x => x.hunk_id === ref.hunk_id);
|
|
2548
|
+
if (b) return [b.l0, b.l1];
|
|
2549
|
+
}
|
|
2550
|
+
let i0 = file.lines.findIndex(l => l.new === ref.lines[0]);
|
|
2551
|
+
let i1 = file.lines.findIndex(l => l.new === ref.lines[1]);
|
|
2552
|
+
if (i0 < 0) i0 = i1 < 0 ? 0 : i1;
|
|
2553
|
+
if (i1 < 0) i1 = i0;
|
|
2554
|
+
return [Math.min(i0, i1), Math.max(i0, i1)];
|
|
2555
|
+
}
|
|
2556
|
+
function wtRecompute() {
|
|
2557
|
+
wtHi = null; wtAnchors = new Set();
|
|
2558
|
+
if (!wtOn || !wt || wtStep < 0 || !file) return;
|
|
2559
|
+
const step = wt.steps[wtStep];
|
|
2560
|
+
if (step.ref.path === curPath) {
|
|
2561
|
+
const [i0, i1] = wtLineRange(step.ref);
|
|
2562
|
+
wtHi = {
|
|
2563
|
+
r0: Math.min(viewOf[i0], viewOf[i1]),
|
|
2564
|
+
r1: Math.max(viewOf[i0], viewOf[i1])
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
for (const ar of step.also) {
|
|
2568
|
+
if (ar.path !== curPath) continue;
|
|
2569
|
+
wtAnchors.add(viewOf[wtLineRange(ar)[0]]);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
function wtScrollTo(r0, r1) {
|
|
2573
|
+
const top = offsets[r0] ?? 0;
|
|
2574
|
+
const bot = offsets[Math.min(r1 + 1, offsets.length - 1)] ?? top;
|
|
2575
|
+
pane.scrollTop = bot - top <= pane.clientHeight
|
|
2576
|
+
? Math.max(0, (top + bot) / 2 - pane.clientHeight / 2)
|
|
2577
|
+
: Math.max(0, top - pane.clientHeight / 4);
|
|
2578
|
+
}
|
|
2579
|
+
async function wtJumpRef(ref) {
|
|
2580
|
+
if (ref.path !== curPath) await openFile(ref.path);
|
|
2581
|
+
const [i0, i1] = wtLineRange(ref);
|
|
2582
|
+
wtScrollTo(viewOf[i0], viewOf[i1]);
|
|
2583
|
+
wtAway = true;
|
|
2584
|
+
renderRows(); drawMinimap();
|
|
2585
|
+
}
|
|
2586
|
+
async function gotoStep(i) {
|
|
2587
|
+
if (!wt || i < 0 || i >= wt.steps.length) return;
|
|
2588
|
+
wtStep = i; wtAway = false;
|
|
2589
|
+
const step = wt.steps[i];
|
|
2590
|
+
if (step.ref.path !== curPath) await openFile(step.ref.path);
|
|
2591
|
+
wtRecompute();
|
|
2592
|
+
if (wtHi) {
|
|
2593
|
+
wtScrollTo(wtHi.r0, wtHi.r1);
|
|
2594
|
+
focusIdx = blockByRow[wtHi.r0] ?? -1;
|
|
2595
|
+
}
|
|
2596
|
+
wtVisited.add(step.id);
|
|
2597
|
+
renderRows(); drawMinimap(); renderWtBar(); renderWtOutline();
|
|
2598
|
+
wtSaveProgress(); updateHash();
|
|
2599
|
+
}
|
|
2600
|
+
function wtMove(dir) {
|
|
2601
|
+
if (!wt) return;
|
|
2602
|
+
if (dir > 0 && wtAway) { gotoStep(wtStep); return; }
|
|
2603
|
+
const seq = wtSeq();
|
|
2604
|
+
const next = dir > 0 ? seq.find(i => i > wtStep)
|
|
2605
|
+
: seq.slice().reverse().find(i => i < wtStep);
|
|
2606
|
+
if (next !== undefined) gotoStep(next);
|
|
2607
|
+
else toast(dir > 0 ? "end of walkthrough" : "start of walkthrough");
|
|
2608
|
+
}
|
|
2609
|
+
function wtActIdx() {
|
|
2610
|
+
const s = wt.steps[wtStep];
|
|
2611
|
+
return s ? wt.acts.findIndex(a => a.id === s.act) : -1;
|
|
2612
|
+
}
|
|
2613
|
+
function wtGotoAct(ai) {
|
|
2614
|
+
if (!wt || ai < 0 || ai >= wt.acts.length) return;
|
|
2615
|
+
const i = wt.steps.findIndex(s => s.act === wt.acts[ai].id);
|
|
2616
|
+
if (i >= 0) gotoStep(i);
|
|
2617
|
+
}
|
|
2618
|
+
function renderWtBar() {
|
|
2619
|
+
if (!wt || wtStep < 0) return;
|
|
2620
|
+
const step = wt.steps[wtStep];
|
|
2621
|
+
const act = wt.acts.find(a => a.id === step.act);
|
|
2622
|
+
$("wt-act").textContent = act ? act.title : step.act;
|
|
2623
|
+
$("wt-act").title = wt.title + (wt.intro ? " — " + wt.intro : "");
|
|
2624
|
+
$("wt-count").textContent = (wtStep + 1) + "/" + wt.steps.length;
|
|
2625
|
+
$("wt-title").textContent = step.title;
|
|
2626
|
+
$("wt-bar").classList.toggle("d1", step.depth === 1);
|
|
2627
|
+
const txt = $("wt-text");
|
|
2628
|
+
txt.classList.toggle("err", !!wt.error);
|
|
2629
|
+
txt.textContent = wt.error ? "walkthrough reload failed:\n" + wt.error : step.text;
|
|
2630
|
+
txt.scrollTop = 0;
|
|
2631
|
+
|
|
2632
|
+
const refs = $("wt-refs");
|
|
2633
|
+
refs.textContent = "";
|
|
2634
|
+
const chip = (label, cls, title, fn) => {
|
|
2635
|
+
const c = document.createElement("span");
|
|
2636
|
+
c.className = "wt-chip " + cls; c.textContent = label; c.title = title;
|
|
2637
|
+
c.onclick = fn; refs.append(c);
|
|
2638
|
+
};
|
|
2639
|
+
chip(wtRefLabel(step.ref), "primary", step.ref.path, () => gotoStep(wtStep));
|
|
2640
|
+
for (const ar of step.also)
|
|
2641
|
+
chip("+ " + wtRefLabel(ar), "", ar.path + (ar.note ? " — " + ar.note : ""),
|
|
2642
|
+
() => wtJumpRef(ar));
|
|
2643
|
+
if (step.forward) {
|
|
2644
|
+
const tgt = wt.steps[wtStepIdx(step.forward)];
|
|
2645
|
+
const f = document.createElement("span");
|
|
2646
|
+
f.className = "wt-fwd";
|
|
2647
|
+
f.textContent = "→ later: " + (tgt ? tgt.title : step.forward);
|
|
2648
|
+
f.onclick = () => gotoStep(wtStepIdx(step.forward));
|
|
2649
|
+
refs.append(f);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
const pips = $("wt-pips");
|
|
2653
|
+
pips.textContent = "";
|
|
2654
|
+
let lastAct = null;
|
|
2655
|
+
wt.steps.forEach((s, i) => {
|
|
2656
|
+
if (lastAct !== null && s.act !== lastAct) {
|
|
2657
|
+
const g = document.createElement("span"); g.className = "wt-gap";
|
|
2658
|
+
pips.append(g);
|
|
2659
|
+
}
|
|
2660
|
+
lastAct = s.act;
|
|
2661
|
+
const p = document.createElement("span");
|
|
2662
|
+
p.className = "wt-pip" + (s.depth ? " d1" : "")
|
|
2663
|
+
+ (i === wtStep ? " cur" : "") + (wtVisited.has(s.id) ? " visited" : "");
|
|
2664
|
+
p.title = s.title;
|
|
2665
|
+
p.onclick = () => gotoStep(i);
|
|
2666
|
+
pips.append(p);
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
function renderWtOutline() {
|
|
2670
|
+
const o = $("wt-outline");
|
|
2671
|
+
if (!wt) { o.hidden = true; return; }
|
|
2672
|
+
o.hidden = false;
|
|
2673
|
+
o.textContent = "";
|
|
2674
|
+
const head = document.createElement("div");
|
|
2675
|
+
head.id = "wt-o-head";
|
|
2676
|
+
const arrow = document.createElement("span");
|
|
2677
|
+
arrow.className = "d-arrow";
|
|
2678
|
+
arrow.textContent = renderWtOutline.closed ? "▸ " : "▾ ";
|
|
2679
|
+
head.append(arrow, wt.title);
|
|
2680
|
+
head.onclick = () => { renderWtOutline.closed = !renderWtOutline.closed; renderWtOutline(); };
|
|
2681
|
+
o.append(head);
|
|
2682
|
+
if (renderWtOutline.closed) return;
|
|
2683
|
+
for (const a of wt.acts) {
|
|
2684
|
+
const ah = document.createElement("div");
|
|
2685
|
+
ah.className = "wt-o-act";
|
|
2686
|
+
ah.textContent = (wtActsClosed.has(a.id) ? "▸ " : "▾ ") + a.title;
|
|
2687
|
+
ah.onclick = () => {
|
|
2688
|
+
wtActsClosed.has(a.id) ? wtActsClosed.delete(a.id) : wtActsClosed.add(a.id);
|
|
2689
|
+
renderWtOutline();
|
|
2690
|
+
};
|
|
2691
|
+
o.append(ah);
|
|
2692
|
+
if (wtActsClosed.has(a.id)) continue;
|
|
2693
|
+
wt.steps.forEach((s, i) => {
|
|
2694
|
+
if (s.act !== a.id) return;
|
|
2695
|
+
const el = document.createElement("div");
|
|
2696
|
+
el.className = "wt-o-step" + (s.depth ? " d1" : "") + (i === wtStep ? " cur" : "");
|
|
2697
|
+
el.textContent = s.title;
|
|
2698
|
+
el.title = s.title;
|
|
2699
|
+
el.onclick = () => { if (!wtOn) setWalkthrough(true); gotoStep(i); };
|
|
2700
|
+
o.append(el);
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function wtSaveProgress() {
|
|
2705
|
+
if (!wt || wtStep < 0) return;
|
|
2706
|
+
savePref("seendiff.wt." + wt.digest, JSON.stringify(
|
|
2707
|
+
{ stepId: wt.steps[wtStep].id, visited: [...wtVisited] }));
|
|
2708
|
+
}
|
|
2709
|
+
function wtRestoreProgress() {
|
|
2710
|
+
try {
|
|
2711
|
+
const p = JSON.parse(localStorage.getItem("seendiff.wt." + wt.digest) || "null");
|
|
2712
|
+
if (p) { wtVisited = new Set(p.visited || []); return wtStepIdx(p.stepId); }
|
|
2713
|
+
} catch (e) { }
|
|
2714
|
+
return -1;
|
|
2715
|
+
}
|
|
2716
|
+
function setWalkthrough(on) {
|
|
2717
|
+
if (!wt) return;
|
|
2718
|
+
wtOn = on;
|
|
2719
|
+
$("wt-bar").hidden = !on;
|
|
2720
|
+
if (on) {
|
|
2721
|
+
if (file) computeOffsets();
|
|
2722
|
+
gotoStep(wtStep >= 0 ? wtStep : Math.max(0, wtRestoreProgress()));
|
|
2723
|
+
} else {
|
|
2724
|
+
wtHi = null; wtAnchors = new Set(); wtAway = false;
|
|
2725
|
+
if (file) { computeOffsets(); renderRows(); drawMinimap(); }
|
|
2726
|
+
updateHash();
|
|
2727
|
+
}
|
|
2728
|
+
renderWtOutline();
|
|
2729
|
+
}
|
|
2730
|
+
$("wt-dismiss").onclick = async () => {
|
|
2731
|
+
try {
|
|
2732
|
+
const r = await fetch("/api/walkthrough/dismiss", { method: "POST" });
|
|
2733
|
+
if (!r.ok) { toast("dismiss failed"); return; }
|
|
2734
|
+
} catch (e) { toast("dismiss failed"); return; }
|
|
2735
|
+
wtClear("walkthrough dismissed");
|
|
2736
|
+
};
|
|
2737
|
+
let wtPollBusy = false;
|
|
2738
|
+
async function wtPoll() {
|
|
2739
|
+
if (!wtWatch || wtPollBusy) return;
|
|
2740
|
+
wtPollBusy = true;
|
|
2741
|
+
try {
|
|
2742
|
+
const r = await fetch("/api/walkthrough");
|
|
2743
|
+
if (!r.ok) return;
|
|
2744
|
+
const nw = await r.json();
|
|
2745
|
+
if (nw.empty) { if (wt) wtClear("walkthrough dismissed"); return; }
|
|
2746
|
+
if (!wt) { wtAdopt(nw); return; }
|
|
2747
|
+
if (nw.digest !== wt.digest) {
|
|
2748
|
+
const curId = wtStep >= 0 ? wt.steps[wtStep].id : null;
|
|
2749
|
+
wt = nw;
|
|
2750
|
+
wtVisited = new Set();
|
|
2751
|
+
const keep = curId ? wtStepIdx(curId) : -1;
|
|
2752
|
+
wtStep = Math.max(0, keep);
|
|
2753
|
+
toast("walkthrough reloaded");
|
|
2754
|
+
renderSidebar();
|
|
2755
|
+
if (wtOn) gotoStep(wtStep); else renderWtOutline();
|
|
2756
|
+
} else if ((nw.error || null) !== (wt.error || null)) {
|
|
2757
|
+
wt.error = nw.error || null;
|
|
2758
|
+
if (wtOn) renderWtBar();
|
|
2759
|
+
}
|
|
2760
|
+
} catch (e) { } finally { wtPollBusy = false; }
|
|
2761
|
+
}
|
|
2762
|
+
function updateHash() {
|
|
2763
|
+
if (!curPath || !file) return;
|
|
2764
|
+
let h = "#f=" + encodeURIComponent(curPath)
|
|
2765
|
+
+ "&l=" + (firstLineOf(findRow(pane.scrollTop)) + 1);
|
|
2766
|
+
if (wtOn && wt && wtStep >= 0)
|
|
2767
|
+
h += "&w=" + encodeURIComponent(wt.steps[wtStep].id);
|
|
2768
|
+
history.replaceState(null, "", h);
|
|
2769
|
+
}
|
|
2770
|
+
{
|
|
2771
|
+
const rz = $("wt-resizer");
|
|
2772
|
+
const saved = parseInt(localStorage.getItem("seendiff.wth") || "160", 10);
|
|
2773
|
+
document.documentElement.style.setProperty("--wt-height", saved + "px");
|
|
2774
|
+
let resizing = false, startY = 0, startH = 0;
|
|
2775
|
+
rz.addEventListener("mousedown", e => {
|
|
2776
|
+
e.preventDefault();
|
|
2777
|
+
resizing = true; rz.classList.add("active");
|
|
2778
|
+
startY = e.clientY; startH = $("wt-bar").getBoundingClientRect().height;
|
|
2779
|
+
document.body.style.cursor = "row-resize";
|
|
2780
|
+
});
|
|
2781
|
+
addEventListener("mousemove", e => {
|
|
2782
|
+
if (!resizing) return;
|
|
2783
|
+
const h = Math.max(80, Math.min(innerHeight * .6, startH + startY - e.clientY));
|
|
2784
|
+
document.documentElement.style.setProperty("--wt-height", h + "px");
|
|
2785
|
+
renderRows(); drawMinimap();
|
|
2786
|
+
});
|
|
2787
|
+
addEventListener("mouseup", () => {
|
|
2788
|
+
if (!resizing) return;
|
|
2789
|
+
resizing = false; rz.classList.remove("active");
|
|
2790
|
+
document.body.style.cursor = "";
|
|
2791
|
+
savePref("seendiff.wth",
|
|
2792
|
+
$("wt-bar").getBoundingClientRect().height | 0);
|
|
2793
|
+
if (file) { computeOffsets(); renderRows(); drawMinimap(); }
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
const SEARCH_MAX = 5000;
|
|
2798
|
+
const PANEL_MAX = 300;
|
|
2799
|
+
let sOpen = false;
|
|
2800
|
+
let sRegex = localStorage.getItem("seendiff.sregex") === "1";
|
|
2801
|
+
let sQuery = "";
|
|
2802
|
+
let sMatches = [];
|
|
2803
|
+
let sByLine = new Map();
|
|
2804
|
+
let sIdx = -1;
|
|
2805
|
+
let sGlobal = null;
|
|
2806
|
+
let sCur = null;
|
|
2807
|
+
let sPanel = true;
|
|
2808
|
+
let sTimer = null;
|
|
2809
|
+
|
|
2810
|
+
function searchRe(q) {
|
|
2811
|
+
if (!q) return null;
|
|
2812
|
+
const flags = "g" + (/[A-Z]/.test(q) ? "" : "i");
|
|
2813
|
+
const body = sRegex ? q : q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2814
|
+
try { return new RegExp(body, flags); } catch (e) { return null; }
|
|
2815
|
+
}
|
|
2816
|
+
function runFileSearch() {
|
|
2817
|
+
sMatches = []; sByLine = new Map(); sIdx = -1;
|
|
2818
|
+
const re = searchRe(sQuery);
|
|
2819
|
+
$("s-input").classList.toggle("bad", !!sQuery && !re);
|
|
2820
|
+
if (!re || !file) return;
|
|
2821
|
+
scan:
|
|
2822
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
2823
|
+
const text = rowText(file.lines[i]);
|
|
2824
|
+
re.lastIndex = 0;
|
|
2825
|
+
let m;
|
|
2826
|
+
while ((m = re.exec(text))) {
|
|
2827
|
+
if (m[0] === "") { re.lastIndex++; continue; }
|
|
2828
|
+
const hit = { i, s: m.index, e: m.index + m[0].length };
|
|
2829
|
+
sMatches.push(hit);
|
|
2830
|
+
if (!sByLine.has(i)) sByLine.set(i, []);
|
|
2831
|
+
sByLine.get(i).push(hit);
|
|
2832
|
+
if (sMatches.length >= SEARCH_MAX) break scan;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
function nearestMatch(fromRow) {
|
|
2837
|
+
for (let k = 0; k < sMatches.length; k++)
|
|
2838
|
+
if (viewOf[sMatches[k].i] >= fromRow) return k;
|
|
2839
|
+
return sMatches.length ? 0 : -1;
|
|
2840
|
+
}
|
|
2841
|
+
function gotoMatch(k) {
|
|
2842
|
+
if (!sMatches.length) { sIdx = -1; redrawSearch(); return; }
|
|
2843
|
+
sIdx = ((k % sMatches.length) + sMatches.length) % sMatches.length;
|
|
2844
|
+
const v = viewOf[sMatches[sIdx].i];
|
|
2845
|
+
const top = offsets[v], bot = offsets[v + 1];
|
|
2846
|
+
if (top < pane.scrollTop || bot > pane.scrollTop + pane.clientHeight)
|
|
2847
|
+
pane.scrollTop = Math.max(0, (top + bot) / 2 - pane.clientHeight / 2);
|
|
2848
|
+
redrawSearch();
|
|
2849
|
+
}
|
|
2850
|
+
function redrawSearch() {
|
|
2851
|
+
clearRows(); renderRows(); drawMinimap(); renderSearchCount(); renderResults();
|
|
2852
|
+
}
|
|
2853
|
+
function renderSearchCount() {
|
|
2854
|
+
const c = $("s-count");
|
|
2855
|
+
if (!sQuery) { c.textContent = ""; return; }
|
|
2856
|
+
if (!sGlobal) { c.textContent = "…"; return; }
|
|
2857
|
+
if (!sGlobal.total) { c.textContent = "no matches"; return; }
|
|
2858
|
+
const tot = sGlobal.total + (sGlobal.truncated ? "+" : "");
|
|
2859
|
+
const k = resultIdx();
|
|
2860
|
+
c.textContent = k >= 0 ? (k + 1) + "/" + tot
|
|
2861
|
+
: tot + " in " + sGlobal.files.length + " files";
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function resultRows() {
|
|
2865
|
+
const out = [];
|
|
2866
|
+
if (sGlobal) for (const f of sGlobal.files)
|
|
2867
|
+
for (const m of f.matches) out.push([f.path, m]);
|
|
2868
|
+
return out;
|
|
2869
|
+
}
|
|
2870
|
+
function resultIdx() {
|
|
2871
|
+
return resultRows().findIndex(([p, m]) => p + "|" + m.line === sCur);
|
|
2872
|
+
}
|
|
2873
|
+
function snippet(el, text, s, e) {
|
|
2874
|
+
el.append(text.slice(0, s).replace(/^\s+/, ""));
|
|
2875
|
+
const mk = document.createElement("mark");
|
|
2876
|
+
mk.textContent = text.slice(s, e);
|
|
2877
|
+
el.append(mk, text.slice(e));
|
|
2878
|
+
}
|
|
2879
|
+
function renderResults() {
|
|
2880
|
+
const p = $("s-panel");
|
|
2881
|
+
p.hidden = !(sOpen && sPanel);
|
|
2882
|
+
$("s-panel-btn").classList.toggle("on", sPanel);
|
|
2883
|
+
if (p.hidden) return;
|
|
2884
|
+
$("sp-sub").textContent = sGlobal ? sGlobal.files.length + " files" : "";
|
|
2885
|
+
const box = $("s-results");
|
|
2886
|
+
box.textContent = "";
|
|
2887
|
+
const note = msg => {
|
|
2888
|
+
const d = document.createElement("div");
|
|
2889
|
+
d.id = "s-note"; d.textContent = msg; box.append(d);
|
|
2890
|
+
};
|
|
2891
|
+
if (!sQuery) { note("type to search"); return; }
|
|
2892
|
+
if (!sGlobal) { note("searching…"); return; }
|
|
2893
|
+
const all = resultRows();
|
|
2894
|
+
if (!all.length) {
|
|
2895
|
+
note("no matches in the change");
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const rows = all.slice(0, PANEL_MAX);
|
|
2899
|
+
let lastPath = null;
|
|
2900
|
+
for (const [path, m] of rows) {
|
|
2901
|
+
if (path !== lastPath) {
|
|
2902
|
+
lastPath = path;
|
|
2903
|
+
const head = document.createElement("div");
|
|
2904
|
+
head.className = "sr-file"; head.title = path;
|
|
2905
|
+
const bdi = document.createElement("bdi");
|
|
2906
|
+
bdi.textContent = path;
|
|
2907
|
+
head.append(bdi);
|
|
2908
|
+
box.append(head);
|
|
2909
|
+
}
|
|
2910
|
+
const key = path + "|" + m.line;
|
|
2911
|
+
const el = document.createElement("div");
|
|
2912
|
+
el.className = "sr-hit " + m.type + (key === sCur ? " cur" : "");
|
|
2913
|
+
const ln = document.createElement("span");
|
|
2914
|
+
ln.className = "sr-ln"; ln.textContent = m.new ?? m.old ?? "";
|
|
2915
|
+
const tx = document.createElement("span");
|
|
2916
|
+
tx.className = "sr-txt";
|
|
2917
|
+
snippet(tx, m.text, m.start, m.end);
|
|
2918
|
+
el.append(ln, tx);
|
|
2919
|
+
el.onclick = () => openResult(path, m);
|
|
2920
|
+
box.append(el);
|
|
2921
|
+
}
|
|
2922
|
+
if (rows.length < all.length)
|
|
2923
|
+
note("showing " + rows.length + " of " + all.length + " lines");
|
|
2924
|
+
else if (sGlobal.truncated)
|
|
2925
|
+
note("stopped at " + sGlobal.total + " matches — narrow the query");
|
|
2926
|
+
box.querySelector(".sr-hit.cur")?.scrollIntoView({ block: "nearest" });
|
|
2927
|
+
}
|
|
2928
|
+
async function openResult(path, m) {
|
|
2929
|
+
sCur = path + "|" + m.line;
|
|
2930
|
+
if (path !== curPath) await openFile(path);
|
|
2931
|
+
const k = nearestMatch(viewOf[m.line] ?? 0);
|
|
2932
|
+
gotoMatch(k >= 0 ? k : 0);
|
|
2933
|
+
}
|
|
2934
|
+
function stepResult(dir) {
|
|
2935
|
+
const rows = resultRows();
|
|
2936
|
+
if (!rows.length) return;
|
|
2937
|
+
let k = rows.findIndex(([p, m]) => p + "|" + m.line === sCur);
|
|
2938
|
+
k = k < 0 ? (dir > 0 ? 0 : rows.length - 1)
|
|
2939
|
+
: ((k + dir) % rows.length + rows.length) % rows.length;
|
|
2940
|
+
openResult(rows[k][0], rows[k][1]);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
function scheduleGlobal() {
|
|
2944
|
+
clearTimeout(sTimer);
|
|
2945
|
+
sTimer = setTimeout(runGlobalSearch, 200);
|
|
2946
|
+
}
|
|
2947
|
+
async function runGlobalSearch() {
|
|
2948
|
+
clearTimeout(sTimer);
|
|
2949
|
+
if (!sQuery) { renderResults(); return; }
|
|
2950
|
+
const q = sQuery;
|
|
2951
|
+
try {
|
|
2952
|
+
const r = await fetch("/api/search?q=" + encodeURIComponent(q)
|
|
2953
|
+
+ "®ex=" + (sRegex ? "true" : "false")
|
|
2954
|
+
+ "&case=" + (/[A-Z]/.test(q) ? "true" : "false"));
|
|
2955
|
+
if (q !== sQuery) return;
|
|
2956
|
+
$("s-input").classList.toggle("bad", !r.ok);
|
|
2957
|
+
sGlobal = r.ok ? await r.json() : { total: 0, truncated: false, files: [] };
|
|
2958
|
+
} catch (e) {
|
|
2959
|
+
sGlobal = { total: 0, truncated: false, files: [] };
|
|
2960
|
+
}
|
|
2961
|
+
if (q !== sQuery) return;
|
|
2962
|
+
renderResults(); renderSearchCount();
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function paneResized() {
|
|
2966
|
+
if (!file) return;
|
|
2967
|
+
computeOffsets(); renderRows(); drawMinimap();
|
|
2968
|
+
}
|
|
2969
|
+
function openSearch() {
|
|
2970
|
+
sOpen = true;
|
|
2971
|
+
$("searchbar").hidden = false;
|
|
2972
|
+
renderResults();
|
|
2973
|
+
paneResized();
|
|
2974
|
+
const inp = $("s-input");
|
|
2975
|
+
inp.value = sQuery;
|
|
2976
|
+
inp.focus(); inp.select();
|
|
2977
|
+
renderSearchCount();
|
|
2978
|
+
}
|
|
2979
|
+
function closeSearch() {
|
|
2980
|
+
if (!sOpen) return;
|
|
2981
|
+
sOpen = false;
|
|
2982
|
+
$("searchbar").hidden = true;
|
|
2983
|
+
$("s-input").blur();
|
|
2984
|
+
renderResults();
|
|
2985
|
+
paneResized();
|
|
2986
|
+
}
|
|
2987
|
+
function queryChanged() {
|
|
2988
|
+
sQuery = $("s-input").value;
|
|
2989
|
+
sGlobal = null; sCur = null;
|
|
2990
|
+
sPanel = true;
|
|
2991
|
+
runFileSearch();
|
|
2992
|
+
if (sMatches.length) gotoMatch(nearestMatch(findRow(pane.scrollTop)));
|
|
2993
|
+
else redrawSearch();
|
|
2994
|
+
scheduleGlobal();
|
|
2995
|
+
}
|
|
2996
|
+
$("s-input").addEventListener("input", queryChanged);
|
|
2997
|
+
$("s-next").onclick = () => stepResult(1);
|
|
2998
|
+
$("s-prev").onclick = () => stepResult(-1);
|
|
2999
|
+
$("s-regex").onclick = () => {
|
|
3000
|
+
sRegex = !sRegex;
|
|
3001
|
+
savePref("seendiff.sregex", sRegex ? "1" : "0");
|
|
3002
|
+
$("s-regex").classList.toggle("on", sRegex);
|
|
3003
|
+
sGlobal = null;
|
|
3004
|
+
runFileSearch();
|
|
3005
|
+
redrawSearch();
|
|
3006
|
+
scheduleGlobal();
|
|
3007
|
+
};
|
|
3008
|
+
$("s-panel-btn").onclick = () => {
|
|
3009
|
+
sPanel = !sPanel;
|
|
3010
|
+
$("s-panel-btn").classList.toggle("on", sPanel);
|
|
3011
|
+
renderResults(); paneResized();
|
|
3012
|
+
};
|
|
3013
|
+
$("sp-close").onclick = () => $("s-panel-btn").onclick();
|
|
3014
|
+
$("s-close").onclick = closeSearch;
|
|
3015
|
+
{
|
|
3016
|
+
const p = $("s-panel"), rz = $("sp-resizer");
|
|
3017
|
+
const saved = parseInt(localStorage.getItem("seendiff.spw") || "0", 10);
|
|
3018
|
+
if (saved) p.style.width = saved + "px";
|
|
3019
|
+
let resizing = false;
|
|
3020
|
+
rz.addEventListener("mousedown", e => {
|
|
3021
|
+
e.preventDefault();
|
|
3022
|
+
resizing = true; rz.classList.add("active");
|
|
3023
|
+
document.body.style.cursor = "col-resize";
|
|
3024
|
+
});
|
|
3025
|
+
addEventListener("mousemove", e => {
|
|
3026
|
+
if (resizing) p.style.width = (innerWidth - e.clientX) + "px";
|
|
3027
|
+
});
|
|
3028
|
+
addEventListener("mouseup", () => {
|
|
3029
|
+
if (!resizing) return;
|
|
3030
|
+
resizing = false; rz.classList.remove("active");
|
|
3031
|
+
document.body.style.cursor = "";
|
|
3032
|
+
savePref("seendiff.spw", p.getBoundingClientRect().width | 0);
|
|
3033
|
+
paneResized();
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
addEventListener("keydown", e => {
|
|
3038
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === "f" || e.key === "F")) {
|
|
3039
|
+
e.preventDefault(); openSearch(); return;
|
|
3040
|
+
}
|
|
3041
|
+
if (e.target === $("s-input")) {
|
|
3042
|
+
if (e.key === "Escape") { e.preventDefault(); closeSearch(); }
|
|
3043
|
+
else if (e.key === "Enter") {
|
|
3044
|
+
e.preventDefault();
|
|
3045
|
+
const dir = e.shiftKey ? -1 : 1;
|
|
3046
|
+
stepResult(dir);
|
|
3047
|
+
}
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
if (e.target === $("filter")) {
|
|
3051
|
+
if (e.key === "Escape") $("filter").blur();
|
|
3052
|
+
if (e.key === "Enter") {
|
|
3053
|
+
const vis = visibleFiles().filter(f =>
|
|
3054
|
+
f.path.toLowerCase().includes($("filter").value.toLowerCase()));
|
|
3055
|
+
if (vis.length) { openFile(vis[0].path); $("filter").blur(); }
|
|
3056
|
+
}
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (e.target.tagName === "INPUT" || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
3060
|
+
const k = e.key;
|
|
3061
|
+
const stop = () => e.preventDefault();
|
|
3062
|
+
const confirming = $("confirm-overlay").classList.contains("show");
|
|
3063
|
+
if (k === "Escape") {
|
|
3064
|
+
toggleHelp(false); $("confirm-overlay").classList.remove("show");
|
|
3065
|
+
closeSearch(); return;
|
|
3066
|
+
}
|
|
3067
|
+
if (confirming) {
|
|
3068
|
+
if (k === "Enter") { stop(); doResetAll(); }
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
if (k === "?") { stop(); toggleHelp(); return; }
|
|
3072
|
+
if ($("help-overlay").classList.contains("show")) return;
|
|
3073
|
+
if (k === "X") { stop(); askResetAll(); return; }
|
|
3074
|
+
if (k === "ArrowLeft") { stop(); wtOn && !e.shiftKey ? wtMove(-1) : nextBlock(-1); }
|
|
3075
|
+
else if (k === "ArrowRight") { stop(); wtOn && !e.shiftKey ? wtMove(1) : nextBlock(1); }
|
|
3076
|
+
else if (k === "ArrowUp") { stop(); nextFile(-1); }
|
|
3077
|
+
else if (k === "ArrowDown") { stop(); nextFile(1); }
|
|
3078
|
+
else if (k === "w") { stop(); if (wt) setWalkthrough(!wtOn); }
|
|
3079
|
+
else if (wtOn && k === "[") { stop(); wtGotoAct(wtActIdx() - 1); }
|
|
3080
|
+
else if (wtOn && k === "]") { stop(); wtGotoAct(wtActIdx() + 1); }
|
|
3081
|
+
else if (wtOn && k === ".") {
|
|
3082
|
+
stop(); wtDetail = !wtDetail;
|
|
3083
|
+
toast(wtDetail ? "detail steps on" : "detail steps off");
|
|
3084
|
+
}
|
|
3085
|
+
else if (wtOn && k >= "1" && k <= "9") { stop(); wtGotoAct(+k - 1); }
|
|
3086
|
+
else if (k === "R") {
|
|
3087
|
+
stop(); if (focusIdx >= 0) {
|
|
3088
|
+
const wasRev = blocks[focusIdx].reviewed;
|
|
3089
|
+
toggleReviewed(focusIdx);
|
|
3090
|
+
if (!wasRev && !wtOn) nextUnreviewed(1);
|
|
3091
|
+
} else if (wtOn) toast("no change block here");
|
|
3092
|
+
}
|
|
3093
|
+
else if (k === "n") { stop(); nextUnreviewed(1); }
|
|
3094
|
+
else if (k === "N") { stop(); nextUnreviewed(-1); }
|
|
3095
|
+
else if (k === "j") pane.scrollTop += LH * 3;
|
|
3096
|
+
else if (k === "k") pane.scrollTop -= LH * 3;
|
|
3097
|
+
else if (k === "d") pane.scrollTop += pane.clientHeight / 2;
|
|
3098
|
+
else if (k === "u") pane.scrollTop -= pane.clientHeight / 2;
|
|
3099
|
+
else if (k === "V") { stop(); reviewSeenInFile(); }
|
|
3100
|
+
else if (k === "f") {
|
|
3101
|
+
stop();
|
|
3102
|
+
const f = meta.files.find(x => x.path === curPath);
|
|
3103
|
+
if (f && f.reviewed < f.blocks) toggleFileReviewed(f);
|
|
3104
|
+
nextFile(1);
|
|
3105
|
+
}
|
|
3106
|
+
else if (k === "y") { stop(); copyRefWithContent(); }
|
|
3107
|
+
else if (k === "Y") { stop(); copyUnreviewedRefs(); }
|
|
3108
|
+
else if (k === "s") { stop(); setSplit(!split); }
|
|
3109
|
+
else if (k === "g") { stop(); $("filter").focus(); $("filter").select(); }
|
|
3110
|
+
else if (k === "h") { stop(); $("btn-hide").onclick(); }
|
|
3111
|
+
else if (k === "t") { stop(); $("btn-theme").onclick(); }
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
let rafPending = false;
|
|
3115
|
+
pane.addEventListener("scroll", () => {
|
|
3116
|
+
if (rafPending) return;
|
|
3117
|
+
rafPending = true;
|
|
3118
|
+
requestAnimationFrame(() => {
|
|
3119
|
+
rafPending = false;
|
|
3120
|
+
renderRows(); drawMinimap(); evalSeen();
|
|
3121
|
+
updateHash();
|
|
3122
|
+
});
|
|
3123
|
+
});
|
|
3124
|
+
addEventListener("resize", () => {
|
|
3125
|
+
if (file) { computeOffsets(); }
|
|
3126
|
+
layoutNotches();
|
|
3127
|
+
if (meta) renderSidebar();
|
|
3128
|
+
renderRows(); drawMinimap();
|
|
3129
|
+
});
|
|
3130
|
+
document.fonts.ready.then(() => {
|
|
3131
|
+
if (file) { measureMetrics(); computeOffsets(); renderRows(); drawMinimap(); }
|
|
3132
|
+
});
|
|
3133
|
+
$("filter").addEventListener("input", renderSidebar);
|
|
3134
|
+
|
|
3135
|
+
|
|
3136
|
+
|
|
3137
|
+
(async function init() {
|
|
3138
|
+
setTheme(document.documentElement.dataset.theme);
|
|
3139
|
+
setFont(localStorage.getItem("seendiff.font") || FONTS[0]);
|
|
3140
|
+
setSplit(split);
|
|
3141
|
+
$("s-regex").classList.toggle("on", sRegex);
|
|
3142
|
+
layoutNotches();
|
|
3143
|
+
await loadMeta();
|
|
3144
|
+
await loadWalkthrough();
|
|
3145
|
+
if (!meta.files.length && !wt) {
|
|
3146
|
+
$("tb-path").textContent = "nothing to review";
|
|
3147
|
+
const d = document.createElement("div");
|
|
3148
|
+
d.id = "empty-msg";
|
|
3149
|
+
d.textContent = `No committed changes between ${meta.base_ref} and HEAD. `
|
|
3150
|
+
+ `Commit your work, or pass a different base ref.`;
|
|
3151
|
+
pane.append(d);
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (wt) { renderSidebar(); renderWtOutline(); }
|
|
3155
|
+
const h = new URLSearchParams(location.hash.slice(1));
|
|
3156
|
+
const start = h.get("f");
|
|
3157
|
+
const known = p => meta.files.some(f => f.path === p)
|
|
3158
|
+
|| (wt && wt.wt_paths.includes(p));
|
|
3159
|
+
if (start && known(start)) {
|
|
3160
|
+
await openFile(start, +(h.get("l") || 0) || undefined);
|
|
3161
|
+
} else if (meta.files.length) {
|
|
3162
|
+
await openFile(meta.files[0].path);
|
|
3163
|
+
}
|
|
3164
|
+
if (wt) {
|
|
3165
|
+
const wparam = h.get("w");
|
|
3166
|
+
const i = wparam ? wtStepIdx(wparam) : -1;
|
|
3167
|
+
if (i >= 0) { wtStep = i; wtVisited = new Set(); wtRestoreProgress(); }
|
|
3168
|
+
setWalkthrough(true);
|
|
3169
|
+
toast(wt.title);
|
|
3170
|
+
}
|
|
3171
|
+
})();
|
|
3172
|
+
</script>
|
|
3173
|
+
</body>
|
|
3174
|
+
|
|
3175
|
+
</html>
|