@chrysb/alphaclaw 0.3.2 → 0.3.3

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.
Files changed (36) hide show
  1. package/bin/alphaclaw.js +29 -2
  2. package/lib/cli/git-sync.js +25 -0
  3. package/lib/public/css/explorer.css +983 -0
  4. package/lib/public/css/shell.css +48 -4
  5. package/lib/public/css/theme.css +6 -1
  6. package/lib/public/icons/folder-line.svg +1 -0
  7. package/lib/public/icons/hashtag.svg +3 -0
  8. package/lib/public/icons/home-5-line.svg +1 -0
  9. package/lib/public/icons/save-fill.svg +3 -0
  10. package/lib/public/js/app.js +259 -158
  11. package/lib/public/js/components/action-button.js +12 -1
  12. package/lib/public/js/components/file-tree.js +322 -0
  13. package/lib/public/js/components/file-viewer.js +691 -0
  14. package/lib/public/js/components/icons.js +182 -0
  15. package/lib/public/js/components/sidebar-git-panel.js +149 -0
  16. package/lib/public/js/components/sidebar.js +272 -0
  17. package/lib/public/js/lib/api.js +26 -0
  18. package/lib/public/js/lib/browse-draft-state.js +109 -0
  19. package/lib/public/js/lib/file-highlighting.js +6 -0
  20. package/lib/public/js/lib/file-tree-utils.js +12 -0
  21. package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
  22. package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
  23. package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
  24. package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
  25. package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
  26. package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
  27. package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
  28. package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
  29. package/lib/public/setup.html +1 -0
  30. package/lib/server/constants.js +1 -0
  31. package/lib/server/onboarding/workspace.js +3 -2
  32. package/lib/server/routes/browse.js +295 -0
  33. package/lib/server.js +24 -3
  34. package/lib/setup/core-prompts/TOOLS.md +3 -1
  35. package/lib/setup/skills/control-ui/SKILL.md +12 -20
  36. package/package.json +1 -1
@@ -0,0 +1,691 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "https://esm.sh/preact/hooks";
9
+ import htm from "https://esm.sh/htm";
10
+ import { marked } from "https://esm.sh/marked";
11
+ import { fetchFileContent, saveFileContent } from "../lib/api.js";
12
+ import {
13
+ formatFrontmatterValue,
14
+ getFileSyntaxKind,
15
+ highlightEditorLines,
16
+ parseFrontmatter,
17
+ } from "../lib/syntax-highlighters/index.js";
18
+ import {
19
+ clearStoredFileDraft,
20
+ readStoredFileDraft,
21
+ updateDraftIndex,
22
+ writeStoredFileDraft,
23
+ } from "../lib/browse-draft-state.js";
24
+ import { ActionButton } from "./action-button.js";
25
+ import { SaveFillIcon } from "./icons.js";
26
+ import { showToast } from "./toast.js";
27
+
28
+ const html = htm.bind(h);
29
+ const kFileViewerModeStorageKey = "alphaclaw.browse.fileViewerMode";
30
+ const kLegacyFileViewerModeStorageKey = "alphaclawBrowseFileViewerMode";
31
+ const kProtectedBrowsePaths = new Set(["openclaw.json", "devices/paired.json"]);
32
+
33
+ const parsePathSegments = (inputPath) =>
34
+ String(inputPath || "")
35
+ .split("/")
36
+ .map((part) => part.trim())
37
+ .filter(Boolean);
38
+
39
+ const normalizePolicyPath = (inputPath) =>
40
+ String(inputPath || "")
41
+ .replaceAll("\\", "/")
42
+ .replace(/^\.\/+/, "")
43
+ .replace(/^\/+/, "")
44
+ .trim()
45
+ .toLowerCase();
46
+
47
+ const readStoredFileViewerMode = () => {
48
+ try {
49
+ const storedMode = String(
50
+ window.localStorage.getItem(kFileViewerModeStorageKey) ||
51
+ window.localStorage.getItem(kLegacyFileViewerModeStorageKey) ||
52
+ "",
53
+ ).trim();
54
+ return storedMode === "preview" ? "preview" : "edit";
55
+ } catch {
56
+ return "edit";
57
+ }
58
+ };
59
+
60
+
61
+ export const FileViewer = ({ filePath = "" }) => {
62
+ const normalizedPath = String(filePath || "").trim();
63
+ const normalizedPolicyPath = normalizePolicyPath(normalizedPath);
64
+ const [content, setContent] = useState("");
65
+ const [initialContent, setInitialContent] = useState("");
66
+ const [viewMode, setViewMode] = useState(readStoredFileViewerMode);
67
+ const [loading, setLoading] = useState(false);
68
+ const [saving, setSaving] = useState(false);
69
+ const [error, setError] = useState("");
70
+ const [isFolderPath, setIsFolderPath] = useState(false);
71
+ const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
72
+ const [protectedEditBypassPaths, setProtectedEditBypassPaths] = useState(
73
+ () => new Set(),
74
+ );
75
+ const editorLineNumbersRef = useRef(null);
76
+ const editorHighlightRef = useRef(null);
77
+ const editorTextareaRef = useRef(null);
78
+ const previewRef = useRef(null);
79
+ const viewScrollRatioRef = useRef(0);
80
+ const isSyncingScrollRef = useRef(false);
81
+ const loadedFilePathRef = useRef("");
82
+ const editorLineNumberRowRefs = useRef([]);
83
+ const editorHighlightLineRefs = useRef([]);
84
+
85
+ const pathSegments = useMemo(
86
+ () => parsePathSegments(normalizedPath),
87
+ [normalizedPath],
88
+ );
89
+ const hasSelectedPath = normalizedPath.length > 0;
90
+ const canEditFile = hasSelectedPath && !isFolderPath;
91
+ const isDirty = canEditFile && content !== initialContent;
92
+ const isProtectedFile =
93
+ canEditFile && kProtectedBrowsePaths.has(normalizedPolicyPath);
94
+ const isProtectedLocked =
95
+ isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
96
+ const syntaxKind = useMemo(
97
+ () => getFileSyntaxKind(normalizedPath),
98
+ [normalizedPath],
99
+ );
100
+ const isMarkdownFile = syntaxKind === "markdown";
101
+ const shouldUseHighlightedEditor = syntaxKind !== "plain";
102
+ const parsedFrontmatter = useMemo(
103
+ () =>
104
+ isMarkdownFile
105
+ ? parseFrontmatter(content)
106
+ : { entries: [], body: content },
107
+ [content, isMarkdownFile],
108
+ );
109
+ const highlightedEditorLines = useMemo(
110
+ () =>
111
+ shouldUseHighlightedEditor
112
+ ? highlightEditorLines(content, syntaxKind)
113
+ : [],
114
+ [content, shouldUseHighlightedEditor, syntaxKind],
115
+ );
116
+ const editorLineNumbers = useMemo(() => {
117
+ const lineCount = String(content || "").split("\n").length;
118
+ return Array.from({ length: lineCount }, (_, index) => index + 1);
119
+ }, [content]);
120
+
121
+ const syncEditorLineNumberHeights = useCallback(() => {
122
+ if (!shouldUseHighlightedEditor || viewMode !== "edit") return;
123
+ const numberRows = editorLineNumberRowRefs.current;
124
+ const highlightRows = editorHighlightLineRefs.current;
125
+ const rowCount = Math.min(numberRows.length, highlightRows.length);
126
+ for (let index = 0; index < rowCount; index += 1) {
127
+ const numberRow = numberRows[index];
128
+ const highlightRow = highlightRows[index];
129
+ if (!numberRow || !highlightRow) continue;
130
+ numberRow.style.height = `${highlightRow.offsetHeight}px`;
131
+ }
132
+ }, [shouldUseHighlightedEditor, viewMode]);
133
+
134
+ useEffect(() => {
135
+ syncEditorLineNumberHeights();
136
+ }, [content, syncEditorLineNumberHeights]);
137
+
138
+ useEffect(() => {
139
+ if (!shouldUseHighlightedEditor || viewMode !== "edit") return () => {};
140
+ const onResize = () => syncEditorLineNumberHeights();
141
+ window.addEventListener("resize", onResize);
142
+ return () => window.removeEventListener("resize", onResize);
143
+ }, [shouldUseHighlightedEditor, viewMode, syncEditorLineNumberHeights]);
144
+ const previewHtml = useMemo(
145
+ () =>
146
+ isMarkdownFile
147
+ ? marked.parse(parsedFrontmatter.body || "", {
148
+ gfm: true,
149
+ breaks: true,
150
+ })
151
+ : "",
152
+ [parsedFrontmatter.body, isMarkdownFile],
153
+ );
154
+
155
+ useEffect(() => {
156
+ if (!isMarkdownFile && viewMode !== "edit") {
157
+ setViewMode("edit");
158
+ }
159
+ }, [isMarkdownFile, viewMode]);
160
+
161
+ useEffect(() => {
162
+ try {
163
+ window.localStorage.setItem(kFileViewerModeStorageKey, viewMode);
164
+ } catch {}
165
+ }, [viewMode]);
166
+
167
+ useEffect(() => {
168
+ let active = true;
169
+ loadedFilePathRef.current = "";
170
+ if (!hasSelectedPath) {
171
+ setContent("");
172
+ setInitialContent("");
173
+ setError("");
174
+ setIsFolderPath(false);
175
+ viewScrollRatioRef.current = 0;
176
+ loadedFilePathRef.current = "";
177
+ return () => {
178
+ active = false;
179
+ };
180
+ }
181
+
182
+ const loadFile = async () => {
183
+ setLoading(true);
184
+ setError("");
185
+ setIsFolderPath(false);
186
+ try {
187
+ const data = await fetchFileContent(normalizedPath);
188
+ if (!active) return;
189
+ const nextContent = data.content || "";
190
+ const draftContent = readStoredFileDraft(normalizedPath);
191
+ setContent(draftContent || nextContent);
192
+ updateDraftIndex(
193
+ normalizedPath,
194
+ Boolean(draftContent && draftContent !== nextContent),
195
+ { dispatchEvent: (event) => window.dispatchEvent(event) },
196
+ );
197
+ setInitialContent(nextContent);
198
+ viewScrollRatioRef.current = 0;
199
+ loadedFilePathRef.current = normalizedPath;
200
+ } catch (loadError) {
201
+ if (!active) return;
202
+ const message = loadError.message || "Could not load file";
203
+ if (/path is not a file/i.test(message)) {
204
+ setContent("");
205
+ setInitialContent("");
206
+ setIsFolderPath(true);
207
+ setError("");
208
+ loadedFilePathRef.current = normalizedPath;
209
+ return;
210
+ }
211
+ setError(message);
212
+ } finally {
213
+ if (active) setLoading(false);
214
+ }
215
+ };
216
+ loadFile();
217
+ return () => {
218
+ active = false;
219
+ };
220
+ }, [hasSelectedPath, normalizedPath]);
221
+
222
+ useEffect(() => {
223
+ if (loadedFilePathRef.current !== normalizedPath) return;
224
+ if (!canEditFile || !hasSelectedPath || loading) return;
225
+ if (content === initialContent) {
226
+ clearStoredFileDraft(normalizedPath);
227
+ updateDraftIndex(normalizedPath, false, {
228
+ dispatchEvent: (event) => window.dispatchEvent(event),
229
+ });
230
+ return;
231
+ }
232
+ writeStoredFileDraft(normalizedPath, content);
233
+ updateDraftIndex(normalizedPath, true, {
234
+ dispatchEvent: (event) => window.dispatchEvent(event),
235
+ });
236
+ }, [
237
+ canEditFile,
238
+ hasSelectedPath,
239
+ loading,
240
+ content,
241
+ initialContent,
242
+ normalizedPath,
243
+ ]);
244
+
245
+ const handleSave = async () => {
246
+ if (!canEditFile || saving || !isDirty || isProtectedLocked) return;
247
+ setSaving(true);
248
+ setError("");
249
+ try {
250
+ const data = await saveFileContent(normalizedPath, content);
251
+ setInitialContent(content);
252
+ clearStoredFileDraft(normalizedPath);
253
+ updateDraftIndex(normalizedPath, false, {
254
+ dispatchEvent: (event) => window.dispatchEvent(event),
255
+ });
256
+ window.dispatchEvent(
257
+ new CustomEvent("alphaclaw:browse-file-saved", {
258
+ detail: { path: normalizedPath },
259
+ }),
260
+ );
261
+ if (data.synced === false) {
262
+ showToast("Saved, but git sync failed", "error");
263
+ } else {
264
+ showToast("Saved and synced", "success");
265
+ }
266
+ } catch (saveError) {
267
+ const message = saveError.message || "Could not save file";
268
+ setError(message);
269
+ showToast(message, "error");
270
+ } finally {
271
+ setSaving(false);
272
+ }
273
+ };
274
+
275
+ const handleEditProtectedFile = () => {
276
+ if (!normalizedPolicyPath) return;
277
+ setProtectedEditBypassPaths((previousPaths) => {
278
+ const nextPaths = new Set(previousPaths);
279
+ nextPaths.add(normalizedPolicyPath);
280
+ return nextPaths;
281
+ });
282
+ };
283
+
284
+ const handleContentInput = (event) => {
285
+ if (isProtectedLocked) return;
286
+ const nextContent = event.target.value;
287
+ setContent(nextContent);
288
+ if (hasSelectedPath && canEditFile) {
289
+ writeStoredFileDraft(normalizedPath, nextContent);
290
+ updateDraftIndex(normalizedPath, nextContent !== initialContent, {
291
+ dispatchEvent: (event) => window.dispatchEvent(event),
292
+ });
293
+ }
294
+ };
295
+
296
+ const getScrollRatio = (element) => {
297
+ if (!element) return 0;
298
+ const maxScrollTop = element.scrollHeight - element.clientHeight;
299
+ if (maxScrollTop <= 0) return 0;
300
+ return element.scrollTop / maxScrollTop;
301
+ };
302
+
303
+ const setScrollByRatio = (element, ratio) => {
304
+ if (!element) return;
305
+ const maxScrollTop = element.scrollHeight - element.clientHeight;
306
+ if (maxScrollTop <= 0) {
307
+ element.scrollTop = 0;
308
+ return;
309
+ }
310
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
311
+ element.scrollTop = maxScrollTop * clampedRatio;
312
+ };
313
+
314
+ const handleEditorScroll = (event) => {
315
+ if (isSyncingScrollRef.current) return;
316
+ const nextScrollTop = event.currentTarget.scrollTop;
317
+ const nextRatio = getScrollRatio(event.currentTarget);
318
+ viewScrollRatioRef.current = nextRatio;
319
+ if (!editorLineNumbersRef.current) return;
320
+ editorLineNumbersRef.current.scrollTop = nextScrollTop;
321
+ if (editorHighlightRef.current) {
322
+ editorHighlightRef.current.scrollTop = nextScrollTop;
323
+ editorHighlightRef.current.scrollLeft = event.currentTarget.scrollLeft;
324
+ }
325
+ if (previewRef.current) {
326
+ isSyncingScrollRef.current = true;
327
+ setScrollByRatio(previewRef.current, nextRatio);
328
+ window.requestAnimationFrame(() => {
329
+ isSyncingScrollRef.current = false;
330
+ });
331
+ }
332
+ };
333
+
334
+ const handlePreviewScroll = (event) => {
335
+ if (isSyncingScrollRef.current) return;
336
+ const nextRatio = getScrollRatio(event.currentTarget);
337
+ viewScrollRatioRef.current = nextRatio;
338
+ isSyncingScrollRef.current = true;
339
+ setScrollByRatio(editorTextareaRef.current, nextRatio);
340
+ setScrollByRatio(editorLineNumbersRef.current, nextRatio);
341
+ setScrollByRatio(editorHighlightRef.current, nextRatio);
342
+ window.requestAnimationFrame(() => {
343
+ isSyncingScrollRef.current = false;
344
+ });
345
+ };
346
+
347
+ const handleChangeViewMode = (nextMode) => {
348
+ if (nextMode === viewMode) return;
349
+ const nextRatio =
350
+ viewMode === "preview"
351
+ ? getScrollRatio(previewRef.current)
352
+ : getScrollRatio(editorTextareaRef.current);
353
+ viewScrollRatioRef.current = nextRatio;
354
+ setViewMode(nextMode);
355
+ window.requestAnimationFrame(() => {
356
+ isSyncingScrollRef.current = true;
357
+ if (nextMode === "preview") {
358
+ setScrollByRatio(previewRef.current, nextRatio);
359
+ } else {
360
+ setScrollByRatio(editorTextareaRef.current, nextRatio);
361
+ setScrollByRatio(editorLineNumbersRef.current, nextRatio);
362
+ setScrollByRatio(editorHighlightRef.current, nextRatio);
363
+ }
364
+ window.requestAnimationFrame(() => {
365
+ isSyncingScrollRef.current = false;
366
+ });
367
+ });
368
+ };
369
+
370
+ if (!hasSelectedPath) {
371
+ return html`
372
+ <div class="file-viewer-empty">
373
+ <div class="file-viewer-empty-mark">[ ]</div>
374
+ <div class="file-viewer-empty-title">
375
+ Browse and edit files<br />Syncs to git
376
+ </div>
377
+ </div>
378
+ `;
379
+ }
380
+
381
+ return html`
382
+ <div class="file-viewer">
383
+ <div class="file-viewer-tabbar">
384
+ <div class="file-viewer-tab active">
385
+ <span class="file-icon">f</span>
386
+ <span class="file-viewer-breadcrumb">
387
+ ${pathSegments.map(
388
+ (segment, index) => html`
389
+ <span class="file-viewer-breadcrumb-item">
390
+ <span
391
+ class=${index === pathSegments.length - 1
392
+ ? "is-current"
393
+ : ""}
394
+ >
395
+ ${segment}
396
+ </span>
397
+ ${index < pathSegments.length - 1 &&
398
+ html`<span class="file-viewer-sep">></span>`}
399
+ </span>
400
+ `,
401
+ )}
402
+ </span>
403
+ ${isDirty
404
+ ? html`<span
405
+ class="file-viewer-dirty-dot"
406
+ aria-hidden="true"
407
+ ></span>`
408
+ : null}
409
+ </div>
410
+ <div class="file-viewer-tabbar-spacer"></div>
411
+ ${isMarkdownFile &&
412
+ html`
413
+ <div class="file-viewer-view-toggle">
414
+ <button
415
+ class=${`file-viewer-view-toggle-button ${viewMode === "edit" ? "active" : ""}`}
416
+ onclick=${() => handleChangeViewMode("edit")}
417
+ >
418
+ edit
419
+ </button>
420
+ <button
421
+ class=${`file-viewer-view-toggle-button ${viewMode === "preview" ? "active" : ""}`}
422
+ onclick=${() => handleChangeViewMode("preview")}
423
+ >
424
+ preview
425
+ </button>
426
+ </div>
427
+ `}
428
+ <${ActionButton}
429
+ onClick=${handleSave}
430
+ disabled=${loading || !isDirty || !canEditFile || isProtectedLocked}
431
+ loading=${saving}
432
+ tone=${isDirty ? "primary" : "secondary"}
433
+ size="sm"
434
+ idleLabel="Save"
435
+ loadingLabel="Saving..."
436
+ idleIcon=${SaveFillIcon}
437
+ idleIconClassName="file-viewer-save-icon"
438
+ className="file-viewer-save-action"
439
+ />
440
+ </div>
441
+ ${isProtectedFile
442
+ ? html`
443
+ <div class="file-viewer-protected-banner">
444
+ <div class="file-viewer-protected-banner-text">
445
+ Protected file. Changes may break workspace behavior.
446
+ </div>
447
+ ${isProtectedLocked
448
+ ? html`
449
+ <${ActionButton}
450
+ onClick=${handleEditProtectedFile}
451
+ tone="warning"
452
+ size="sm"
453
+ idleLabel="Edit anyway"
454
+ />
455
+ `
456
+ : null}
457
+ </div>
458
+ `
459
+ : null}
460
+ ${isMarkdownFile && parsedFrontmatter.entries.length > 0
461
+ ? html`
462
+ <div class="frontmatter-box">
463
+ <button
464
+ type="button"
465
+ class="frontmatter-title"
466
+ onclick=${() =>
467
+ setFrontmatterCollapsed((collapsed) => !collapsed)}
468
+ >
469
+ <span
470
+ class=${`frontmatter-chevron ${frontmatterCollapsed ? "" : "open"}`}
471
+ aria-hidden="true"
472
+ >
473
+ <svg viewBox="0 0 20 20" focusable="false">
474
+ <path d="M7 4l6 6-6 6" />
475
+ </svg>
476
+ </span>
477
+ <span>frontmatter</span>
478
+ </button>
479
+ ${!frontmatterCollapsed
480
+ ? html`
481
+ <div class="frontmatter-grid">
482
+ ${parsedFrontmatter.entries.map((entry) => {
483
+ const formattedValue = formatFrontmatterValue(
484
+ entry.rawValue,
485
+ );
486
+ const isMultilineValue = formattedValue.includes("\n");
487
+ return html`
488
+ <div class="frontmatter-row" key=${entry.key}>
489
+ <div class="frontmatter-key">${entry.key}</div>
490
+ ${isMultilineValue
491
+ ? html`
492
+ <pre
493
+ class="frontmatter-value frontmatter-value-pre"
494
+ >
495
+ ${formattedValue}</pre
496
+ >
497
+ `
498
+ : html`<div class="frontmatter-value">
499
+ ${formattedValue}
500
+ </div>`}
501
+ </div>
502
+ `;
503
+ })}
504
+ </div>
505
+ `
506
+ : null}
507
+ </div>
508
+ `
509
+ : null}
510
+ ${loading
511
+ ? html`<div class="file-viewer-state">Loading file...</div>`
512
+ : error
513
+ ? html`<div class="file-viewer-state file-viewer-state-error">
514
+ ${error}
515
+ </div>`
516
+ : isFolderPath
517
+ ? html`
518
+ <div class="file-viewer-state">
519
+ Folder selected. Choose a file from this folder in the tree.
520
+ </div>
521
+ `
522
+ : html`
523
+ ${isMarkdownFile
524
+ ? html`
525
+ <div
526
+ class=${`file-viewer-preview ${viewMode === "preview" ? "" : "file-viewer-pane-hidden"}`}
527
+ ref=${previewRef}
528
+ onscroll=${handlePreviewScroll}
529
+ aria-hidden=${viewMode === "preview" ? "false" : "true"}
530
+ dangerouslySetInnerHTML=${{ __html: previewHtml }}
531
+ ></div>
532
+ <div
533
+ class=${`file-viewer-editor-shell ${viewMode === "edit" ? "" : "file-viewer-pane-hidden"}`}
534
+ aria-hidden=${viewMode === "edit" ? "false" : "true"}
535
+ >
536
+ <div
537
+ class="file-viewer-editor-line-num-col"
538
+ ref=${editorLineNumbersRef}
539
+ >
540
+ ${editorLineNumbers.map(
541
+ (lineNumber) => html`
542
+ <div
543
+ class="file-viewer-editor-line-num"
544
+ key=${lineNumber}
545
+ ref=${(element) => {
546
+ editorLineNumberRowRefs.current[
547
+ lineNumber - 1
548
+ ] = element;
549
+ }}
550
+ >
551
+ ${lineNumber}
552
+ </div>
553
+ `,
554
+ )}
555
+ </div>
556
+ <div class="file-viewer-editor-stack">
557
+ <div
558
+ class="file-viewer-editor-highlight"
559
+ ref=${editorHighlightRef}
560
+ >
561
+ ${highlightedEditorLines.map(
562
+ (line) => html`
563
+ <div
564
+ class="file-viewer-editor-highlight-line"
565
+ key=${line.lineNumber}
566
+ ref=${(element) => {
567
+ editorHighlightLineRefs.current[
568
+ line.lineNumber - 1
569
+ ] = element;
570
+ }}
571
+ >
572
+ <span
573
+ class="file-viewer-editor-highlight-line-content"
574
+ dangerouslySetInnerHTML=${{
575
+ __html: line.html,
576
+ }}
577
+ ></span>
578
+ </div>
579
+ `,
580
+ )}
581
+ </div>
582
+ <textarea
583
+ class="file-viewer-editor file-viewer-editor-overlay"
584
+ ref=${editorTextareaRef}
585
+ value=${content}
586
+ onInput=${handleContentInput}
587
+ onScroll=${handleEditorScroll}
588
+ spellcheck=${false}
589
+ autocorrect="off"
590
+ autocapitalize="off"
591
+ autocomplete="off"
592
+ data-gramm="false"
593
+ data-gramm_editor="false"
594
+ data-enable-grammarly="false"
595
+ wrap="soft"
596
+ ></textarea>
597
+ </div>
598
+ </div>
599
+ `
600
+ : html`
601
+ <div class="file-viewer-editor-shell">
602
+ <div
603
+ class="file-viewer-editor-line-num-col"
604
+ ref=${editorLineNumbersRef}
605
+ >
606
+ ${editorLineNumbers.map(
607
+ (lineNumber) => html`
608
+ <div
609
+ class="file-viewer-editor-line-num"
610
+ key=${lineNumber}
611
+ ref=${(element) => {
612
+ editorLineNumberRowRefs.current[
613
+ lineNumber - 1
614
+ ] = element;
615
+ }}
616
+ >
617
+ ${lineNumber}
618
+ </div>
619
+ `,
620
+ )}
621
+ </div>
622
+ ${shouldUseHighlightedEditor
623
+ ? html`
624
+ <div class="file-viewer-editor-stack">
625
+ <div
626
+ class="file-viewer-editor-highlight"
627
+ ref=${editorHighlightRef}
628
+ >
629
+ ${highlightedEditorLines.map(
630
+ (line) => html`
631
+ <div
632
+ class="file-viewer-editor-highlight-line"
633
+ key=${line.lineNumber}
634
+ ref=${(element) => {
635
+ editorHighlightLineRefs.current[
636
+ line.lineNumber - 1
637
+ ] = element;
638
+ }}
639
+ >
640
+ <span
641
+ class="file-viewer-editor-highlight-line-content"
642
+ dangerouslySetInnerHTML=${{
643
+ __html: line.html,
644
+ }}
645
+ ></span>
646
+ </div>
647
+ `,
648
+ )}
649
+ </div>
650
+ <textarea
651
+ class="file-viewer-editor file-viewer-editor-overlay"
652
+ ref=${editorTextareaRef}
653
+ value=${content}
654
+ onInput=${handleContentInput}
655
+ onScroll=${handleEditorScroll}
656
+ readonly=${isProtectedLocked}
657
+ spellcheck=${false}
658
+ autocorrect="off"
659
+ autocapitalize="off"
660
+ autocomplete="off"
661
+ data-gramm="false"
662
+ data-gramm_editor="false"
663
+ data-enable-grammarly="false"
664
+ wrap="soft"
665
+ ></textarea>
666
+ </div>
667
+ `
668
+ : html`
669
+ <textarea
670
+ class="file-viewer-editor"
671
+ ref=${editorTextareaRef}
672
+ value=${content}
673
+ onInput=${handleContentInput}
674
+ onScroll=${handleEditorScroll}
675
+ readonly=${isProtectedLocked}
676
+ spellcheck=${false}
677
+ autocorrect="off"
678
+ autocapitalize="off"
679
+ autocomplete="off"
680
+ data-gramm="false"
681
+ data-gramm_editor="false"
682
+ data-enable-grammarly="false"
683
+ wrap="soft"
684
+ ></textarea>
685
+ `}
686
+ </div>
687
+ `}
688
+ `}
689
+ </div>
690
+ `;
691
+ };