@fresh-editor/fresh-editor 0.2.23 → 0.2.25

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.
@@ -0,0 +1,596 @@
1
+ /// <reference path="./fresh.d.ts" />
2
+
3
+ /**
4
+ * Shared git history rendering helpers used by the git log plugin and the
5
+ * review-diff plugin's branch review mode.
6
+ *
7
+ * All rendering uses theme-keyed colours (`syntax.keyword`, `editor.fg`, etc.)
8
+ * so the panels stay consistent with the editor's current theme. The entry
9
+ * builders produce `TextPropertyEntry[]` lists whose sub-ranges are styled
10
+ * via `inlineOverlays` — no separate imperative overlay pass is required.
11
+ */
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ export interface GitCommit {
18
+ hash: string;
19
+ shortHash: string;
20
+ author: string;
21
+ authorEmail: string;
22
+ date: string;
23
+ relativeDate: string;
24
+ subject: string;
25
+ body: string;
26
+ refs: string;
27
+ }
28
+
29
+ export interface FetchGitLogOptions {
30
+ /** Max commits to fetch (default: 200). */
31
+ maxCommits?: number;
32
+ /** Optional revision range (e.g. "main..HEAD"). Defaults to HEAD. */
33
+ range?: string;
34
+ /** Working directory. Defaults to `editor.getCwd()`. */
35
+ cwd?: string;
36
+ }
37
+
38
+ export interface BuildCommitLogEntriesOptions {
39
+ /** Index of the "selected" row — rendered with the selected-bg highlight. */
40
+ selectedIndex?: number;
41
+ /** Optional header string (e.g. "Commits:"). `null` omits the header row. */
42
+ header?: string | null;
43
+ /** Footer line (status hint). Omitted when null/undefined. */
44
+ footer?: string | null;
45
+ /** Target width for padding column alignment (default 0 = no padding). */
46
+ width?: number;
47
+ /** "log" property-type prefix for entries (default "log-commit"). */
48
+ propertyType?: string;
49
+ }
50
+
51
+ // =============================================================================
52
+ // Theme keys
53
+ // =============================================================================
54
+
55
+ export const GIT_THEME = {
56
+ header: "syntax.keyword" as OverlayColorSpec,
57
+ separator: "ui.split_separator_fg" as OverlayColorSpec,
58
+ hash: "syntax.number" as OverlayColorSpec,
59
+ author: "syntax.function" as OverlayColorSpec,
60
+ date: "syntax.string" as OverlayColorSpec,
61
+ subject: "editor.fg" as OverlayColorSpec,
62
+ subjectMuted: "editor.line_number_fg" as OverlayColorSpec,
63
+ refBranch: "syntax.type" as OverlayColorSpec,
64
+ refRemote: "syntax.function" as OverlayColorSpec,
65
+ refTag: "syntax.number" as OverlayColorSpec,
66
+ refHead: "syntax.keyword" as OverlayColorSpec,
67
+ diffAdd: "editor.diff_add_bg" as OverlayColorSpec,
68
+ diffRemove: "editor.diff_remove_bg" as OverlayColorSpec,
69
+ diffAddFg: "diagnostic.info_fg" as OverlayColorSpec,
70
+ diffRemoveFg: "diagnostic.error_fg" as OverlayColorSpec,
71
+ diffHunk: "syntax.type" as OverlayColorSpec,
72
+ metaLabel: "editor.line_number_fg" as OverlayColorSpec,
73
+ selectionBg: "editor.selection_bg" as OverlayColorSpec,
74
+ sectionBg: "editor.current_line_bg" as OverlayColorSpec,
75
+ footer: "editor.line_number_fg" as OverlayColorSpec,
76
+ };
77
+
78
+ // =============================================================================
79
+ // Author initials helper — compact "(AL)" / "(JD)" style label used in the
80
+ // aligned log view. Falls back to the raw author when no initials can be
81
+ // extracted.
82
+ // =============================================================================
83
+
84
+ export function authorInitials(author: string): string {
85
+ const cleaned = author.replace(/[<>].*/g, "").trim();
86
+ const parts = cleaned.split(/\s+/).filter(p => p.length > 0);
87
+ if (parts.length === 0) return "??";
88
+ if (parts.length === 1) {
89
+ return parts[0].slice(0, 2).toUpperCase();
90
+ }
91
+ const first = parts[0][0] || "?";
92
+ const last = parts[parts.length - 1][0] || "?";
93
+ return (first + last).toUpperCase();
94
+ }
95
+
96
+ // =============================================================================
97
+ // Commit fetching
98
+ // =============================================================================
99
+
100
+ export async function fetchGitLog(
101
+ editor: EditorAPI,
102
+ opts: FetchGitLogOptions = {}
103
+ ): Promise<GitCommit[]> {
104
+ const maxCommits = opts.maxCommits ?? 200;
105
+ const cwd = opts.cwd ?? editor.getCwd();
106
+ const format = "%H%x00%h%x00%an%x00%ae%x00%ai%x00%ar%x00%D%x00%s%x00%b%x1e";
107
+ const args = ["log", `--format=${format}`, `-n${maxCommits}`];
108
+ if (opts.range) args.push(opts.range);
109
+
110
+ const result = await editor.spawnProcess("git", args, cwd);
111
+ if (result.exit_code !== 0) return [];
112
+
113
+ const commits: GitCommit[] = [];
114
+ const records = result.stdout.split("\x1e");
115
+ for (const record of records) {
116
+ if (!record.trim()) continue;
117
+ const parts = record.split("\x00");
118
+ if (parts.length < 8) continue;
119
+ commits.push({
120
+ hash: parts[0].trim(),
121
+ shortHash: parts[1].trim(),
122
+ author: parts[2].trim(),
123
+ authorEmail: parts[3].trim(),
124
+ date: parts[4].trim(),
125
+ relativeDate: parts[5].trim(),
126
+ refs: parts[6].trim(),
127
+ subject: parts[7].trim(),
128
+ body: parts[8] ? parts[8].trim() : "",
129
+ });
130
+ }
131
+ return commits;
132
+ }
133
+
134
+ /**
135
+ * A single file's diff exceeding this line count is omitted from the
136
+ * rendered `git show` output. Generated files (lockfiles, bundled SVGs,
137
+ * minified JS) can produce megabyte-scale diffs that balloon the detail
138
+ * panel into hundreds of thousands of entries — slow to render and not
139
+ * useful to read. The stat header still lists the file so the user knows
140
+ * it changed; a footer tells them which ones were skipped.
141
+ */
142
+ const MAX_DIFF_LINES_PER_FILE = 2000;
143
+
144
+ export async function fetchCommitShow(
145
+ editor: EditorAPI,
146
+ hash: string,
147
+ cwd?: string
148
+ ): Promise<string> {
149
+ const workdir = cwd ?? editor.getCwd();
150
+
151
+ // numstat first — small output, lets us spot oversized files before
152
+ // pulling the full diff.
153
+ const numstatResult = await editor.spawnProcess(
154
+ "git",
155
+ ["show", "--numstat", "--format=", hash],
156
+ workdir
157
+ );
158
+ const oversized: string[] = [];
159
+ if (numstatResult.exit_code === 0) {
160
+ for (const line of numstatResult.stdout.split("\n")) {
161
+ if (!line) continue;
162
+ // numstat format: "<added>\t<removed>\t<path>"; "-" for binary files.
163
+ const tab1 = line.indexOf("\t");
164
+ const tab2 = tab1 >= 0 ? line.indexOf("\t", tab1 + 1) : -1;
165
+ if (tab1 < 0 || tab2 < 0) continue;
166
+ const addedStr = line.slice(0, tab1);
167
+ const removedStr = line.slice(tab1 + 1, tab2);
168
+ const path = line.slice(tab2 + 1);
169
+ const added = addedStr === "-" ? 0 : parseInt(addedStr, 10) || 0;
170
+ const removed = removedStr === "-" ? 0 : parseInt(removedStr, 10) || 0;
171
+ if (added + removed > MAX_DIFF_LINES_PER_FILE) {
172
+ oversized.push(path);
173
+ }
174
+ }
175
+ }
176
+
177
+ // Stat + patch, excluding oversized paths. `:(exclude,top)` is rooted
178
+ // at the repo root so it matches regardless of git's cwd.
179
+ const showArgs = ["show", "--stat", "--patch", hash];
180
+ if (oversized.length > 0) {
181
+ showArgs.push("--", ".");
182
+ for (const p of oversized) showArgs.push(`:(exclude,top)${p}`);
183
+ }
184
+ const result = await editor.spawnProcess("git", showArgs, workdir);
185
+ if (result.exit_code !== 0) return result.stderr || "(no output)";
186
+
187
+ if (oversized.length === 0) return result.stdout;
188
+
189
+ const plural = oversized.length === 1 ? "" : "s";
190
+ let footer = `\n[${oversized.length} large file${plural} omitted from diff (>${MAX_DIFF_LINES_PER_FILE} lines changed):\n`;
191
+ for (const p of oversized) footer += ` ${p}\n`;
192
+ footer += `Run \`git show ${hash.slice(0, 12)} -- <path>\` to view.]\n`;
193
+ return result.stdout + footer;
194
+ }
195
+
196
+ // =============================================================================
197
+ // UTF-8 byte-length helper — the runtime's overlay offsets are in bytes, but
198
+ // JS strings are UTF-16. Colocated here so consumers don't have to redefine it.
199
+ // =============================================================================
200
+
201
+ export function byteLength(s: string): number {
202
+ let b = 0;
203
+ for (let i = 0; i < s.length; i++) {
204
+ const code = s.charCodeAt(i);
205
+ if (code <= 0x7f) b += 1;
206
+ else if (code <= 0x7ff) b += 2;
207
+ else if (code >= 0xd800 && code <= 0xdfff) {
208
+ b += 4;
209
+ i++;
210
+ } else b += 3;
211
+ }
212
+ return b;
213
+ }
214
+
215
+ // =============================================================================
216
+ // Commit log entry building
217
+ // =============================================================================
218
+
219
+ /**
220
+ * Compute column widths for the aligned commit-log table. Returns widths for
221
+ * (hash, date, initials) columns. Subject and refs fill the remainder.
222
+ */
223
+ function commitLogColumnWidths(commits: GitCommit[]): {
224
+ hashW: number;
225
+ dateW: number;
226
+ authorW: number;
227
+ } {
228
+ let hashW = 7;
229
+ let dateW = 10;
230
+ let authorW = 2;
231
+ for (const c of commits) {
232
+ if (c.shortHash.length > hashW) hashW = c.shortHash.length;
233
+ if (c.relativeDate.length > dateW) dateW = c.relativeDate.length;
234
+ const ini = authorInitials(c.author);
235
+ if (ini.length > authorW) authorW = ini.length;
236
+ }
237
+ // Clamp so a pathological author/date doesn't swallow the subject column.
238
+ if (hashW > 12) hashW = 12;
239
+ if (dateW > 16) dateW = 16;
240
+ if (authorW > 4) authorW = 4;
241
+ return { hashW, dateW, authorW };
242
+ }
243
+
244
+ /**
245
+ * Classify a git ref decoration tag so it can be coloured appropriately.
246
+ * Matches a single comma-separated entry from `%D` output, e.g.
247
+ * "HEAD -> main", "origin/main", "tag: v1.0".
248
+ */
249
+ function refTokenColor(token: string): OverlayColorSpec {
250
+ const t = token.trim();
251
+ if (t.startsWith("tag:")) return GIT_THEME.refTag;
252
+ if (t.startsWith("HEAD")) return GIT_THEME.refHead;
253
+ if (t.includes("/")) return GIT_THEME.refRemote;
254
+ return GIT_THEME.refBranch;
255
+ }
256
+
257
+ /**
258
+ * Build a styled commit-log entry row with aligned columns. All styling uses
259
+ * `inlineOverlays` with theme keys — no imperative overlay pass needed.
260
+ */
261
+ function buildCommitRowEntry(
262
+ commit: GitCommit,
263
+ index: number,
264
+ isSelected: boolean,
265
+ widths: { hashW: number; dateW: number; authorW: number },
266
+ propertyType: string
267
+ ): TextPropertyEntry {
268
+ const shortHash = commit.shortHash.padEnd(widths.hashW);
269
+ const date = commit.relativeDate.padEnd(widths.dateW);
270
+ const ini = authorInitials(commit.author).padEnd(widths.authorW);
271
+
272
+ const prefix = " ";
273
+ let byte = byteLength(prefix);
274
+ let text = prefix;
275
+ const overlays: InlineOverlay[] = [];
276
+
277
+ // Hash column
278
+ overlays.push({
279
+ start: byte,
280
+ end: byte + byteLength(shortHash),
281
+ style: { fg: GIT_THEME.hash, bold: true },
282
+ });
283
+ text += shortHash;
284
+ byte += byteLength(shortHash);
285
+
286
+ // Space
287
+ text += " ";
288
+ byte += 2;
289
+
290
+ // Date column
291
+ overlays.push({
292
+ start: byte,
293
+ end: byte + byteLength(date),
294
+ style: { fg: GIT_THEME.date },
295
+ });
296
+ text += date;
297
+ byte += byteLength(date);
298
+
299
+ // Space
300
+ text += " ";
301
+ byte += 2;
302
+
303
+ // Author initials in parentheses
304
+ const authorOpen = "(";
305
+ const authorClose = ")";
306
+ text += authorOpen;
307
+ byte += byteLength(authorOpen);
308
+ overlays.push({
309
+ start: byte,
310
+ end: byte + byteLength(ini),
311
+ style: { fg: GIT_THEME.author, bold: true },
312
+ });
313
+ text += ini;
314
+ byte += byteLength(ini);
315
+ text += authorClose;
316
+ byte += byteLength(authorClose);
317
+
318
+ // Space
319
+ text += " ";
320
+ byte += 1;
321
+
322
+ // Subject
323
+ overlays.push({
324
+ start: byte,
325
+ end: byte + byteLength(commit.subject),
326
+ style: { fg: GIT_THEME.subject },
327
+ });
328
+ text += commit.subject;
329
+ byte += byteLength(commit.subject);
330
+
331
+ // Refs (if any) — tokenise and colour each separately. %D returns a
332
+ // comma-separated list like "HEAD -> main, origin/main, tag: v1".
333
+ if (commit.refs) {
334
+ text += " ";
335
+ byte += 2;
336
+ const tokens = commit.refs.split(",").map(t => t.trim()).filter(t => t.length > 0);
337
+ for (let i = 0; i < tokens.length; i++) {
338
+ if (i > 0) {
339
+ text += " ";
340
+ byte += 1;
341
+ }
342
+ // "HEAD -> main" renders as two logical tokens inside one entry;
343
+ // treat the whole token as one coloured chunk for simplicity.
344
+ const t = tokens[i];
345
+ const bracket = `[${t}]`;
346
+ overlays.push({
347
+ start: byte,
348
+ end: byte + byteLength(bracket),
349
+ style: { fg: refTokenColor(t), bold: true },
350
+ });
351
+ text += bracket;
352
+ byte += byteLength(bracket);
353
+ }
354
+ }
355
+
356
+ const finalText = text + "\n";
357
+
358
+ const style: Partial<OverlayOptions> = isSelected
359
+ ? { bg: GIT_THEME.selectionBg, extendToLineEnd: true, bold: true }
360
+ : {};
361
+
362
+ return {
363
+ text: finalText,
364
+ properties: {
365
+ type: propertyType,
366
+ index,
367
+ hash: commit.hash,
368
+ shortHash: commit.shortHash,
369
+ author: commit.author,
370
+ date: commit.relativeDate,
371
+ subject: commit.subject,
372
+ refs: commit.refs,
373
+ },
374
+ style,
375
+ inlineOverlays: overlays,
376
+ };
377
+ }
378
+
379
+ export function buildCommitLogEntries(
380
+ commits: GitCommit[],
381
+ opts: BuildCommitLogEntriesOptions = {}
382
+ ): TextPropertyEntry[] {
383
+ const header = opts.header === undefined ? "Commits:" : opts.header;
384
+ const footer = opts.footer;
385
+ const selectedIndex = opts.selectedIndex ?? -1;
386
+ const propertyType = opts.propertyType ?? "log-commit";
387
+
388
+ const entries: TextPropertyEntry[] = [];
389
+
390
+ if (header !== null) {
391
+ entries.push({
392
+ text: header + "\n",
393
+ properties: { type: "log-header" },
394
+ style: { fg: GIT_THEME.header, bold: true, underline: true },
395
+ });
396
+ }
397
+
398
+ if (commits.length === 0) {
399
+ entries.push({
400
+ text: " (no commits)\n",
401
+ properties: { type: "log-empty" },
402
+ style: { fg: GIT_THEME.metaLabel, italic: true },
403
+ });
404
+ } else {
405
+ const widths = commitLogColumnWidths(commits);
406
+ for (let i = 0; i < commits.length; i++) {
407
+ entries.push(
408
+ buildCommitRowEntry(commits[i], i, i === selectedIndex, widths, propertyType)
409
+ );
410
+ }
411
+ }
412
+
413
+ if (footer) {
414
+ entries.push({
415
+ text: "\n",
416
+ properties: { type: "log-blank" },
417
+ });
418
+ entries.push({
419
+ text: footer + "\n",
420
+ properties: { type: "log-footer" },
421
+ style: { fg: GIT_THEME.footer, italic: true },
422
+ });
423
+ }
424
+
425
+ return entries;
426
+ }
427
+
428
+ // =============================================================================
429
+ // Commit detail (git show) entry building
430
+ // =============================================================================
431
+
432
+ interface DetailBuildContext {
433
+ currentFile: string | null;
434
+ currentNewLine: number;
435
+ }
436
+
437
+ /**
438
+ * Style a single line from `git show --stat --patch` output as a styled
439
+ * TextPropertyEntry with inlineOverlays. Tracks file/line context for click
440
+ * navigation.
441
+ */
442
+ function buildDetailLineEntry(
443
+ line: string,
444
+ ctx: DetailBuildContext
445
+ ): TextPropertyEntry {
446
+ const props: Record<string, unknown> = { type: "detail-line" };
447
+ const overlays: InlineOverlay[] = [];
448
+ let lineStyle: Partial<OverlayOptions> = {};
449
+
450
+ // "diff --git a/... b/..."
451
+ const diffHeader = line.match(/^diff --git a\/(.+) b\/(.+)$/);
452
+ if (diffHeader) {
453
+ ctx.currentFile = diffHeader[2];
454
+ ctx.currentNewLine = 0;
455
+ props.type = "detail-diff-header";
456
+ props.file = ctx.currentFile;
457
+ lineStyle = { fg: GIT_THEME.header, bold: true };
458
+ } else if (line.startsWith("+++ b/")) {
459
+ ctx.currentFile = line.slice(6);
460
+ props.type = "detail-diff-header";
461
+ props.file = ctx.currentFile;
462
+ lineStyle = { fg: GIT_THEME.header, bold: true };
463
+ } else if (line.startsWith("+++ ") || line.startsWith("--- ") || line.startsWith("index ")) {
464
+ props.type = "detail-diff-header";
465
+ lineStyle = { fg: GIT_THEME.subjectMuted };
466
+ } else if (line.startsWith("@@")) {
467
+ const hunkMatch = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
468
+ if (hunkMatch) ctx.currentNewLine = parseInt(hunkMatch[1], 10);
469
+ props.type = "detail-hunk-header";
470
+ props.file = ctx.currentFile;
471
+ props.line = ctx.currentNewLine;
472
+ lineStyle = { fg: GIT_THEME.diffHunk, bold: true, extendToLineEnd: true };
473
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
474
+ props.type = "detail-add";
475
+ props.file = ctx.currentFile;
476
+ props.line = ctx.currentNewLine;
477
+ ctx.currentNewLine++;
478
+ lineStyle = { fg: GIT_THEME.diffAddFg, bg: GIT_THEME.diffAdd, extendToLineEnd: true };
479
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
480
+ props.type = "detail-remove";
481
+ props.file = ctx.currentFile;
482
+ lineStyle = { fg: GIT_THEME.diffRemoveFg, bg: GIT_THEME.diffRemove, extendToLineEnd: true };
483
+ } else if (line.startsWith(" ") && ctx.currentFile && ctx.currentNewLine > 0) {
484
+ props.type = "detail-context";
485
+ props.file = ctx.currentFile;
486
+ props.line = ctx.currentNewLine;
487
+ ctx.currentNewLine++;
488
+ } else if (line.startsWith("commit ")) {
489
+ props.type = "detail-commit-line";
490
+ const hashMatch = line.match(/^commit ([a-f0-9]+)/);
491
+ if (hashMatch) {
492
+ props.hash = hashMatch[1];
493
+ // Colour just "commit" and the hash chunk separately.
494
+ const commitWord = "commit ";
495
+ overlays.push({
496
+ start: 0,
497
+ end: byteLength(commitWord),
498
+ style: { fg: GIT_THEME.metaLabel, bold: true },
499
+ });
500
+ overlays.push({
501
+ start: byteLength(commitWord),
502
+ end: byteLength(commitWord) + byteLength(hashMatch[1]),
503
+ style: { fg: GIT_THEME.hash, bold: true },
504
+ });
505
+ }
506
+ } else if (/^(Author|Date|Commit|Merge|AuthorDate|CommitDate):/.test(line)) {
507
+ const colonIdx = line.indexOf(":");
508
+ props.type = "detail-meta";
509
+ overlays.push({
510
+ start: 0,
511
+ end: byteLength(line.slice(0, colonIdx + 1)),
512
+ style: { fg: GIT_THEME.metaLabel, bold: true },
513
+ });
514
+ const fieldKey = line.slice(0, colonIdx).toLowerCase();
515
+ if (fieldKey === "author") {
516
+ overlays.push({
517
+ start: byteLength(line.slice(0, colonIdx + 1)),
518
+ end: byteLength(line),
519
+ style: { fg: GIT_THEME.author },
520
+ });
521
+ } else if (fieldKey.includes("date")) {
522
+ overlays.push({
523
+ start: byteLength(line.slice(0, colonIdx + 1)),
524
+ end: byteLength(line),
525
+ style: { fg: GIT_THEME.date },
526
+ });
527
+ }
528
+ }
529
+
530
+ return {
531
+ text: line + "\n",
532
+ properties: props,
533
+ style: lineStyle,
534
+ inlineOverlays: overlays,
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Build the entries for a commit detail view — a colourful replay of
540
+ * `git show --stat --patch`. The commit message body is already reflowed
541
+ * by `fetchCommitShow`; stat lines and diff lines pass through unchanged.
542
+ */
543
+ export function buildCommitDetailEntries(
544
+ commit: GitCommit | null,
545
+ showOutput: string,
546
+ opts: { footer?: string | null } = {}
547
+ ): TextPropertyEntry[] {
548
+ const entries: TextPropertyEntry[] = [];
549
+
550
+ if (commit) {
551
+ entries.push({
552
+ text: `${commit.shortHash} ${commit.subject}\n`,
553
+ properties: { type: "detail-title", hash: commit.hash },
554
+ style: { fg: GIT_THEME.header, bold: true, underline: true },
555
+ });
556
+ }
557
+
558
+ const ctx: DetailBuildContext = { currentFile: null, currentNewLine: 0 };
559
+ for (const line of showOutput.split("\n")) {
560
+ entries.push(buildDetailLineEntry(line, ctx));
561
+ }
562
+
563
+ const footer = opts.footer;
564
+ if (footer) {
565
+ entries.push({
566
+ text: "\n",
567
+ properties: { type: "detail-blank" },
568
+ });
569
+ entries.push({
570
+ text: footer + "\n",
571
+ properties: { type: "detail-footer" },
572
+ style: { fg: GIT_THEME.footer, italic: true },
573
+ });
574
+ }
575
+
576
+ return entries;
577
+ }
578
+
579
+ // =============================================================================
580
+ // Placeholder entries shown in the detail panel while no commit has been
581
+ // loaded yet (e.g. during initial render or when the log is empty).
582
+ // =============================================================================
583
+
584
+ export function buildDetailPlaceholderEntries(message: string): TextPropertyEntry[] {
585
+ return [
586
+ {
587
+ text: "\n",
588
+ properties: { type: "detail-blank" },
589
+ },
590
+ {
591
+ text: " " + message + "\n",
592
+ properties: { type: "detail-placeholder" },
593
+ style: { fg: GIT_THEME.metaLabel, italic: true },
594
+ },
595
+ ];
596
+ }