pi-studio 0.5.44 → 0.5.46
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/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/client/studio-client.js +2521 -189
- package/client/studio.css +411 -6
- package/index.ts +447 -19
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -47,10 +47,13 @@
|
|
|
47
47
|
const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
|
|
48
48
|
const sourceTextEl = document.getElementById("sourceText");
|
|
49
49
|
const sourceHighlightEl = document.getElementById("sourceHighlight");
|
|
50
|
+
const reviewNoteGutterEl = document.getElementById("reviewNoteGutter");
|
|
51
|
+
const reviewNoteGutterContentEl = document.getElementById("reviewNoteGutterContent");
|
|
50
52
|
const lineNumberGutterEl = document.getElementById("lineNumberGutter");
|
|
51
53
|
const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
|
|
52
54
|
const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
|
|
53
55
|
const sourcePreviewEl = document.getElementById("sourcePreview");
|
|
56
|
+
const editorSelectionCommentBtn = document.getElementById("editorSelectionCommentBtn");
|
|
54
57
|
const leftPaneEl = document.getElementById("leftPane");
|
|
55
58
|
const rightPaneEl = document.getElementById("rightPane");
|
|
56
59
|
const sourceBadgeEl = document.getElementById("sourceBadge");
|
|
@@ -98,6 +101,7 @@
|
|
|
98
101
|
const compactBtn = document.getElementById("compactBtn");
|
|
99
102
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
100
103
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
104
|
+
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
101
105
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
102
106
|
const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
|
|
103
107
|
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
@@ -108,16 +112,35 @@
|
|
|
108
112
|
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
109
113
|
const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
|
|
110
114
|
const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
|
|
115
|
+
const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
|
|
116
|
+
const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
|
|
117
|
+
const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
|
|
118
|
+
const reviewNotesListEl = document.getElementById("reviewNotesList");
|
|
119
|
+
const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
|
|
120
|
+
const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
|
|
121
|
+
const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
|
|
122
|
+
const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
|
|
123
|
+
const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
|
|
111
124
|
|
|
112
125
|
const studioMode = (document.body && document.body.dataset && document.body.dataset.studioMode) === "editor-only"
|
|
113
126
|
? "editor-only"
|
|
114
127
|
: "full";
|
|
115
128
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
116
129
|
|
|
130
|
+
const initialQueryParams = new URLSearchParams(window.location.search || "");
|
|
131
|
+
const explicitDocumentIdentityFromUrl = initialQueryParams.has("docSource")
|
|
132
|
+
|| initialQueryParams.has("docLabel")
|
|
133
|
+
|| initialQueryParams.has("docPath")
|
|
134
|
+
|| initialQueryParams.has("draftId");
|
|
117
135
|
const initialSourceState = {
|
|
118
|
-
source: (
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
source: initialQueryParams.get("docSource")
|
|
137
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialSource) || "blank"),
|
|
138
|
+
label: initialQueryParams.get("docLabel")
|
|
139
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialLabel) || "blank"),
|
|
140
|
+
path: initialQueryParams.get("docPath")
|
|
141
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialPath) || null),
|
|
142
|
+
draftId: initialQueryParams.get("draftId")
|
|
143
|
+
|| ((document.body && document.body.dataset && document.body.dataset.initialDraftId) || null),
|
|
121
144
|
};
|
|
122
145
|
|
|
123
146
|
let ws = null;
|
|
@@ -206,6 +229,7 @@
|
|
|
206
229
|
source: initialSourceState.source,
|
|
207
230
|
label: initialSourceState.label,
|
|
208
231
|
path: initialSourceState.path,
|
|
232
|
+
draftId: initialSourceState.draftId,
|
|
209
233
|
};
|
|
210
234
|
let fileBackedBaselineText = null;
|
|
211
235
|
let activePane = "left";
|
|
@@ -255,7 +279,6 @@
|
|
|
255
279
|
const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
|
|
256
280
|
const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
|
|
257
281
|
const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
|
|
258
|
-
const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
|
|
259
282
|
const PREVIEW_INPUT_DEBOUNCE_MS = 0;
|
|
260
283
|
const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
|
|
261
284
|
const previewPendingTimers = new WeakMap();
|
|
@@ -274,6 +297,16 @@
|
|
|
274
297
|
let annotationsEnabled = true;
|
|
275
298
|
let scratchpadText = "";
|
|
276
299
|
let scratchpadReturnFocusEl = null;
|
|
300
|
+
let scratchpadPersistTimer = null;
|
|
301
|
+
let scratchpadLoadNonce = 0;
|
|
302
|
+
let reviewNotes = [];
|
|
303
|
+
let reviewNotesReturnFocusEl = null;
|
|
304
|
+
let reviewNotesPersistTimer = null;
|
|
305
|
+
let reviewNotesLoadNonce = 0;
|
|
306
|
+
let pendingReviewNoteFocusId = null;
|
|
307
|
+
let pendingReviewNoteInlineFocusId = null;
|
|
308
|
+
let activePreviewCommentSelection = null;
|
|
309
|
+
const previewJumpHighlightState = new WeakMap();
|
|
277
310
|
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
278
311
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
279
312
|
if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
|
|
@@ -740,7 +773,8 @@
|
|
|
740
773
|
if (terminalActivityPhase === "responding") {
|
|
741
774
|
if (activeKind === "critique") return "Critiquing…";
|
|
742
775
|
if (activeKind === "annotation") return "Replying…";
|
|
743
|
-
return "
|
|
776
|
+
if (activeKind === "direct") return "Thinking…";
|
|
777
|
+
return "Working…";
|
|
744
778
|
}
|
|
745
779
|
|
|
746
780
|
if (activeKind) return getTitleActionMessage(activeKind);
|
|
@@ -756,29 +790,26 @@
|
|
|
756
790
|
}
|
|
757
791
|
|
|
758
792
|
function buildStudioFaviconHref() {
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
const accent = readThemeColor("--accent", fg);
|
|
793
|
+
const idleColor = readThemeColor("--text", "#111111");
|
|
794
|
+
const accent = readThemeColor("--accent", "#2563eb");
|
|
762
795
|
const ok = readThemeColor("--ok", "#16a34a");
|
|
763
|
-
const warn = readThemeColor("--warn",
|
|
796
|
+
const warn = readThemeColor("--warn", "#d97706");
|
|
764
797
|
const error = readThemeColor("--error", "#dc2626");
|
|
765
798
|
|
|
766
|
-
let
|
|
767
|
-
|
|
799
|
+
let piColor = idleColor;
|
|
768
800
|
if (titleAttentionMessage) {
|
|
769
|
-
|
|
801
|
+
piColor = ok;
|
|
770
802
|
} else if (wsState === "Disconnected") {
|
|
771
|
-
|
|
803
|
+
piColor = error;
|
|
772
804
|
} else if (wsState === "Connecting") {
|
|
773
|
-
|
|
805
|
+
piColor = accent;
|
|
774
806
|
} else if (getTitleBusyMessage()) {
|
|
775
|
-
|
|
807
|
+
piColor = warn;
|
|
776
808
|
}
|
|
777
809
|
|
|
778
810
|
const svg = [
|
|
779
811
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">',
|
|
780
|
-
`<text x="32" y="35" text-anchor="middle" dominant-baseline="middle" font-size="50" font-weight="700" font-family="ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" fill="${
|
|
781
|
-
badgeSvg,
|
|
812
|
+
`<text x="32" y="35" text-anchor="middle" dominant-baseline="middle" font-size="50" font-weight="700" font-family="ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" fill="${piColor}">π</text>`,
|
|
782
813
|
"</svg>",
|
|
783
814
|
].join("");
|
|
784
815
|
return "data:image/svg+xml," + encodeURIComponent(svg);
|
|
@@ -938,6 +969,12 @@
|
|
|
938
969
|
function updateSourceBadge() {
|
|
939
970
|
const label = sourceState && sourceState.label ? sourceState.label : "blank";
|
|
940
971
|
sourceBadgeEl.textContent = "Editor origin: " + label;
|
|
972
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
973
|
+
if (sourceBadgeEl) {
|
|
974
|
+
sourceBadgeEl.title = descriptor.fileBacked
|
|
975
|
+
? ("Editor origin: " + label + "\nClick to reset origin and detach the current editor text into a new draft. The file on disk will not be changed.")
|
|
976
|
+
: ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
|
|
977
|
+
}
|
|
941
978
|
// Show "Set working dir" button when not file-backed
|
|
942
979
|
var isFileBacked = hasRefreshableFilePath();
|
|
943
980
|
if (isFileBacked) {
|
|
@@ -961,6 +998,26 @@
|
|
|
961
998
|
}
|
|
962
999
|
}
|
|
963
1000
|
|
|
1001
|
+
function resetEditorOrigin() {
|
|
1002
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
1003
|
+
const message = descriptor.fileBacked
|
|
1004
|
+
? ("Reset editor origin and detach the current text from\n\n" + descriptor.label + "\n\ninto a new draft? The file on disk will not be changed, and the current scratchpad/review notes will carry into the new draft.")
|
|
1005
|
+
: ("Reset editor origin and start a new independent draft? The current editor text, scratchpad, and review notes will carry into the new draft.");
|
|
1006
|
+
if (!window.confirm(message)) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const nextLabel = String(sourceTextEl.value || "").trim() ? "draft" : "blank";
|
|
1010
|
+
setSourceState({
|
|
1011
|
+
source: "blank",
|
|
1012
|
+
label: nextLabel,
|
|
1013
|
+
path: null,
|
|
1014
|
+
draftId: makeStudioDraftId(),
|
|
1015
|
+
}, {
|
|
1016
|
+
carryCurrentMetadataToNewDocument: true,
|
|
1017
|
+
});
|
|
1018
|
+
setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
|
|
1019
|
+
}
|
|
1020
|
+
|
|
964
1021
|
function updatePaneFocusButtons() {
|
|
965
1022
|
[
|
|
966
1023
|
[leftFocusBtn, "left"],
|
|
@@ -1062,6 +1119,12 @@
|
|
|
1062
1119
|
&& typeof scratchpadDialogEl.contains === "function"
|
|
1063
1120
|
&& scratchpadDialogEl.contains(event.target)
|
|
1064
1121
|
);
|
|
1122
|
+
const reviewNotesOwnsEvent = Boolean(
|
|
1123
|
+
reviewNotesDialogEl
|
|
1124
|
+
&& event.target
|
|
1125
|
+
&& typeof reviewNotesDialogEl.contains === "function"
|
|
1126
|
+
&& reviewNotesDialogEl.contains(event.target)
|
|
1127
|
+
);
|
|
1065
1128
|
|
|
1066
1129
|
if (isScratchpadOpen() && plainEscape) {
|
|
1067
1130
|
event.preventDefault();
|
|
@@ -1069,7 +1132,13 @@
|
|
|
1069
1132
|
return;
|
|
1070
1133
|
}
|
|
1071
1134
|
|
|
1072
|
-
if (
|
|
1135
|
+
if (isReviewNotesOpen() && plainEscape) {
|
|
1136
|
+
event.preventDefault();
|
|
1137
|
+
closeReviewNotes();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent) {
|
|
1073
1142
|
return;
|
|
1074
1143
|
}
|
|
1075
1144
|
|
|
@@ -2462,6 +2531,7 @@
|
|
|
2462
2531
|
if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
|
|
2463
2532
|
}
|
|
2464
2533
|
|
|
2534
|
+
clearPreviewJumpHighlight(targetEl);
|
|
2465
2535
|
finishPreviewRender(targetEl);
|
|
2466
2536
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
|
|
2467
2537
|
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
@@ -2475,6 +2545,15 @@
|
|
|
2475
2545
|
await renderMermaidInElement(targetEl);
|
|
2476
2546
|
await renderMathFallbackInElement(targetEl);
|
|
2477
2547
|
|
|
2548
|
+
const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
|
|
2549
|
+
&& (
|
|
2550
|
+
(pane === "source" && editorView === "preview")
|
|
2551
|
+
|| (pane === "response" && rightView === "editor-preview")
|
|
2552
|
+
);
|
|
2553
|
+
if (shouldDecoratePreviewComments) {
|
|
2554
|
+
decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2478
2557
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
2479
2558
|
if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
|
|
2480
2559
|
var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
|
|
@@ -2496,6 +2575,7 @@
|
|
|
2496
2575
|
}
|
|
2497
2576
|
|
|
2498
2577
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
2578
|
+
clearPreviewJumpHighlight(targetEl);
|
|
2499
2579
|
finishPreviewRender(targetEl);
|
|
2500
2580
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
|
|
2501
2581
|
if (pane === "response") {
|
|
@@ -2545,7 +2625,7 @@
|
|
|
2545
2625
|
if (editorHighlightEnabled && editorView === "markdown") {
|
|
2546
2626
|
scheduleEditorHighlightRender();
|
|
2547
2627
|
}
|
|
2548
|
-
if (
|
|
2628
|
+
if (editorView === "markdown") {
|
|
2549
2629
|
scheduleEditorLineNumberRender();
|
|
2550
2630
|
}
|
|
2551
2631
|
if (rightView === "editor-preview") {
|
|
@@ -2772,6 +2852,7 @@
|
|
|
2772
2852
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
2773
2853
|
|
|
2774
2854
|
fileInput.disabled = uiBusy;
|
|
2855
|
+
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
2775
2856
|
saveAsBtn.disabled = uiBusy;
|
|
2776
2857
|
saveOverBtn.disabled = uiBusy || !canSaveOver;
|
|
2777
2858
|
if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
|
|
@@ -2805,23 +2886,42 @@
|
|
|
2805
2886
|
syncActionButtons();
|
|
2806
2887
|
}
|
|
2807
2888
|
|
|
2808
|
-
function setSourceState(next) {
|
|
2889
|
+
function setSourceState(next, options) {
|
|
2890
|
+
const previousDescriptor = getCurrentStudioDocumentDescriptor();
|
|
2891
|
+
const nextPath = next && next.path ? next.path : null;
|
|
2809
2892
|
sourceState = {
|
|
2810
2893
|
source: next && next.source ? next.source : "blank",
|
|
2811
2894
|
label: next && next.label ? next.label : "blank",
|
|
2812
|
-
path:
|
|
2895
|
+
path: nextPath,
|
|
2896
|
+
draftId: nextPath
|
|
2897
|
+
? null
|
|
2898
|
+
: (next && next.draftId ? next.draftId : makeStudioDraftId()),
|
|
2813
2899
|
};
|
|
2814
2900
|
if (!sourceState.path) {
|
|
2815
2901
|
clearFileBackedBaseline();
|
|
2816
2902
|
}
|
|
2903
|
+
updateStudioDocumentUrlState(sourceState);
|
|
2817
2904
|
updateSourceBadge();
|
|
2818
2905
|
syncActionButtons();
|
|
2906
|
+
updateScratchpadUi();
|
|
2907
|
+
updateReviewNotesUi();
|
|
2908
|
+
loadScratchpadForCurrentDocument({
|
|
2909
|
+
previousDescriptor: previousDescriptor,
|
|
2910
|
+
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
2911
|
+
});
|
|
2912
|
+
void loadReviewNotesForCurrentDocument({
|
|
2913
|
+
previousDescriptor: previousDescriptor,
|
|
2914
|
+
carryCurrentMetadataToNewDocument: Boolean(options && options.carryCurrentMetadataToNewDocument),
|
|
2915
|
+
});
|
|
2819
2916
|
}
|
|
2820
2917
|
|
|
2821
2918
|
function setEditorText(nextText, options) {
|
|
2822
2919
|
const value = String(nextText || "");
|
|
2823
2920
|
const preserveScroll = Boolean(options && options.preserveScroll);
|
|
2824
2921
|
const preserveSelection = Boolean(options && options.preserveSelection);
|
|
2922
|
+
if (activePreviewCommentSelection) {
|
|
2923
|
+
clearPreviewCommentSelection();
|
|
2924
|
+
}
|
|
2825
2925
|
const previousScrollTop = sourceTextEl.scrollTop;
|
|
2826
2926
|
const previousScrollLeft = sourceTextEl.scrollLeft;
|
|
2827
2927
|
const previousSelectionStart = sourceTextEl.selectionStart;
|
|
@@ -2848,7 +2948,7 @@
|
|
|
2848
2948
|
schedule(() => {
|
|
2849
2949
|
syncEditorHighlightScroll();
|
|
2850
2950
|
});
|
|
2851
|
-
if (
|
|
2951
|
+
if (editorView === "markdown") {
|
|
2852
2952
|
scheduleEditorLineNumberRender();
|
|
2853
2953
|
}
|
|
2854
2954
|
|
|
@@ -2860,6 +2960,7 @@
|
|
|
2860
2960
|
if (!options || options.updateMeta !== false) {
|
|
2861
2961
|
scheduleEditorMetaUpdate();
|
|
2862
2962
|
}
|
|
2963
|
+
updateEditorSelectionCommentUi();
|
|
2863
2964
|
}
|
|
2864
2965
|
|
|
2865
2966
|
function setEditorView(nextView) {
|
|
@@ -2878,6 +2979,7 @@
|
|
|
2878
2979
|
}
|
|
2879
2980
|
|
|
2880
2981
|
if (!showPreview) {
|
|
2982
|
+
clearPreviewJumpHighlight(sourcePreviewEl);
|
|
2881
2983
|
finishPreviewRender(sourcePreviewEl);
|
|
2882
2984
|
}
|
|
2883
2985
|
|
|
@@ -2888,9 +2990,11 @@
|
|
|
2888
2990
|
updateEditorHighlightState();
|
|
2889
2991
|
syncHighlightSelectUi();
|
|
2890
2992
|
updateLineNumberGutterVisibility();
|
|
2891
|
-
if (!showPreview
|
|
2993
|
+
if (!showPreview) {
|
|
2892
2994
|
scheduleEditorLineNumberRender();
|
|
2893
2995
|
}
|
|
2996
|
+
updateReviewNotesUi();
|
|
2997
|
+
updateEditorSelectionCommentUi();
|
|
2894
2998
|
}
|
|
2895
2999
|
|
|
2896
3000
|
function setRightView(nextView) {
|
|
@@ -2905,6 +3009,9 @@
|
|
|
2905
3009
|
window.clearTimeout(responseEditorPreviewTimer);
|
|
2906
3010
|
responseEditorPreviewTimer = null;
|
|
2907
3011
|
}
|
|
3012
|
+
if (rightView !== "editor-preview") {
|
|
3013
|
+
clearPreviewJumpHighlight(critiqueViewEl);
|
|
3014
|
+
}
|
|
2908
3015
|
|
|
2909
3016
|
refreshResponseUi();
|
|
2910
3017
|
syncActionButtons();
|
|
@@ -2921,27 +3028,53 @@
|
|
|
2921
3028
|
);
|
|
2922
3029
|
}
|
|
2923
3030
|
|
|
3031
|
+
function reviewNoteGutterShouldBeVisible() {
|
|
3032
|
+
return Boolean(
|
|
3033
|
+
editorView === "markdown"
|
|
3034
|
+
&& sourceEditorWrapEl
|
|
3035
|
+
&& reviewNoteGutterEl
|
|
3036
|
+
&& reviewNoteGutterContentEl
|
|
3037
|
+
&& lineNumberMeasureEl
|
|
3038
|
+
&& Array.isArray(reviewNotes)
|
|
3039
|
+
&& reviewNotes.length > 0,
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
2924
3043
|
function getEditorLineNumberGutterWidthCss(lineCount) {
|
|
2925
3044
|
const digits = Math.max(2, String(Math.max(1, lineCount || 0)).length);
|
|
2926
3045
|
return "calc(" + digits + "ch + 18px)";
|
|
2927
3046
|
}
|
|
2928
3047
|
|
|
2929
3048
|
function updateLineNumberGutterVisibility() {
|
|
2930
|
-
const
|
|
3049
|
+
const lineNumbersVisible = lineNumbersShouldBeVisible();
|
|
3050
|
+
const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
|
|
3051
|
+
const anyVisible = lineNumbersVisible || reviewMarkersVisible;
|
|
2931
3052
|
if (sourceEditorWrapEl) {
|
|
2932
|
-
sourceEditorWrapEl.classList.toggle("line-numbers-enabled",
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3053
|
+
sourceEditorWrapEl.classList.toggle("line-numbers-enabled", lineNumbersVisible);
|
|
3054
|
+
sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
|
|
3055
|
+
sourceEditorWrapEl.style.setProperty(
|
|
3056
|
+
"--editor-line-number-gutter-width",
|
|
3057
|
+
lineNumbersVisible
|
|
3058
|
+
? getEditorLineNumberGutterWidthCss(Math.max(1, String(sourceTextEl.value || "").replace(/\r\n/g, "\n").split("\n").length))
|
|
3059
|
+
: "0px",
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
if (reviewNoteGutterEl) {
|
|
3063
|
+
reviewNoteGutterEl.hidden = !reviewMarkersVisible;
|
|
2936
3064
|
}
|
|
2937
3065
|
if (lineNumberGutterEl) {
|
|
2938
|
-
lineNumberGutterEl.hidden = !
|
|
3066
|
+
lineNumberGutterEl.hidden = !lineNumbersVisible;
|
|
3067
|
+
}
|
|
3068
|
+
if (!reviewMarkersVisible && reviewNoteGutterContentEl) {
|
|
3069
|
+
reviewNoteGutterContentEl.innerHTML = "";
|
|
2939
3070
|
}
|
|
2940
|
-
if (!
|
|
2941
|
-
|
|
2942
|
-
if (lineNumberMeasureEl) lineNumberMeasureEl.innerHTML = "";
|
|
3071
|
+
if (!lineNumbersVisible && lineNumberGutterContentEl) {
|
|
3072
|
+
lineNumberGutterContentEl.innerHTML = "";
|
|
2943
3073
|
}
|
|
2944
|
-
|
|
3074
|
+
if (!anyVisible && lineNumberMeasureEl) {
|
|
3075
|
+
lineNumberMeasureEl.innerHTML = "";
|
|
3076
|
+
}
|
|
3077
|
+
return anyVisible;
|
|
2945
3078
|
}
|
|
2946
3079
|
|
|
2947
3080
|
function renderEditorLineNumbersNow() {
|
|
@@ -2950,7 +3083,16 @@
|
|
|
2950
3083
|
const text = String(sourceTextEl.value || "").replace(/\r\n/g, "\n");
|
|
2951
3084
|
const lines = text.split("\n");
|
|
2952
3085
|
const lineCount = Math.max(1, lines.length);
|
|
2953
|
-
|
|
3086
|
+
const lineNumbersVisible = lineNumbersShouldBeVisible();
|
|
3087
|
+
const reviewMarkersVisible = reviewNoteGutterShouldBeVisible();
|
|
3088
|
+
|
|
3089
|
+
if (sourceEditorWrapEl) {
|
|
3090
|
+
sourceEditorWrapEl.style.setProperty("--editor-review-note-gutter-width", reviewMarkersVisible ? "28px" : "0px");
|
|
3091
|
+
sourceEditorWrapEl.style.setProperty(
|
|
3092
|
+
"--editor-line-number-gutter-width",
|
|
3093
|
+
lineNumbersVisible ? getEditorLineNumberGutterWidthCss(lineCount) : "0px",
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
2954
3096
|
|
|
2955
3097
|
const styles = window.getComputedStyle(sourceTextEl);
|
|
2956
3098
|
const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
|
|
@@ -2960,24 +3102,107 @@
|
|
|
2960
3102
|
const paddingLeft = parseFloat(styles.paddingLeft) || 0;
|
|
2961
3103
|
const contentWidth = Math.max(1, sourceTextEl.clientWidth - paddingLeft - paddingRight);
|
|
2962
3104
|
|
|
2963
|
-
lineNumberGutterContentEl
|
|
2964
|
-
|
|
3105
|
+
if (lineNumberGutterContentEl) {
|
|
3106
|
+
lineNumberGutterContentEl.style.paddingTop = paddingTop + "px";
|
|
3107
|
+
lineNumberGutterContentEl.style.paddingBottom = paddingBottom + "px";
|
|
3108
|
+
}
|
|
3109
|
+
if (reviewNoteGutterContentEl) {
|
|
3110
|
+
reviewNoteGutterContentEl.style.paddingTop = paddingTop + "px";
|
|
3111
|
+
reviewNoteGutterContentEl.style.paddingBottom = paddingBottom + "px";
|
|
3112
|
+
}
|
|
2965
3113
|
lineNumberMeasureEl.style.width = contentWidth + "px";
|
|
2966
3114
|
lineNumberMeasureEl.innerHTML = lines
|
|
2967
3115
|
.map((line) => "<div class='editor-line-number-measure-line'>" + (line.length ? escapeHtml(line) : "​") + "</div>")
|
|
2968
3116
|
.join("");
|
|
2969
3117
|
|
|
2970
3118
|
const measureLines = Array.from(lineNumberMeasureEl.children);
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3119
|
+
const reviewNoteLineMap = reviewMarkersVisible ? buildReviewNoteLineMap(text) : null;
|
|
3120
|
+
|
|
3121
|
+
if (lineNumbersVisible && lineNumberGutterContentEl) {
|
|
3122
|
+
lineNumberGutterContentEl.innerHTML = measureLines
|
|
3123
|
+
.map((lineEl, index) => {
|
|
3124
|
+
const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
|
|
3125
|
+
return "<div class='editor-line-number-row' style='height:" + height.toFixed(2) + "px'>" + (index + 1) + "</div>";
|
|
3126
|
+
})
|
|
3127
|
+
.join("");
|
|
3128
|
+
} else if (lineNumberGutterContentEl) {
|
|
3129
|
+
lineNumberGutterContentEl.innerHTML = "";
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
if (reviewMarkersVisible && reviewNoteGutterContentEl && reviewNoteLineMap) {
|
|
3133
|
+
reviewNoteGutterContentEl.innerHTML = measureLines
|
|
3134
|
+
.map((lineEl, index) => {
|
|
3135
|
+
const height = Math.max(lineHeightPx, lineEl.getBoundingClientRect().height || 0);
|
|
3136
|
+
const lineNumber = index + 1;
|
|
3137
|
+
const notesForLine = reviewNoteLineMap.get(lineNumber) || [];
|
|
3138
|
+
const count = notesForLine.length;
|
|
3139
|
+
if (count <= 0) {
|
|
3140
|
+
return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'></div>";
|
|
3141
|
+
}
|
|
3142
|
+
const title = count === 1
|
|
3143
|
+
? ("1 local comment on line " + lineNumber + ". Open comments.")
|
|
3144
|
+
: (count + " local comments on line " + lineNumber + ". Open comments.");
|
|
3145
|
+
const markerLabel = count > 9 ? "9+" : (count > 1 ? String(count) : "•");
|
|
3146
|
+
return "<div class='editor-review-note-row' style='height:" + height.toFixed(2) + "px'><button type='button' class='editor-review-note-marker"
|
|
3147
|
+
+ (count > 1 ? " has-multiple" : "")
|
|
3148
|
+
+ "' data-review-note-id='" + escapeHtml(notesForLine[0].id) + "' title='" + escapeHtml(title) + "' aria-label='" + escapeHtml(title) + "'>"
|
|
3149
|
+
+ escapeHtml(markerLabel)
|
|
3150
|
+
+ "</button></div>";
|
|
3151
|
+
})
|
|
3152
|
+
.join("");
|
|
3153
|
+
} else if (reviewNoteGutterContentEl) {
|
|
3154
|
+
reviewNoteGutterContentEl.innerHTML = "";
|
|
3155
|
+
}
|
|
2977
3156
|
|
|
2978
3157
|
syncEditorHighlightScroll();
|
|
2979
3158
|
}
|
|
2980
3159
|
|
|
3160
|
+
function scrollEditorRangeIntoView(range) {
|
|
3161
|
+
if (!range || editorView !== "markdown") return;
|
|
3162
|
+
renderEditorLineNumbersNow();
|
|
3163
|
+
|
|
3164
|
+
const text = String(sourceTextEl.value || "");
|
|
3165
|
+
const startLine = getLineNumberAtOffset(text, range.start);
|
|
3166
|
+
const endLine = getLineNumberAtOffset(text, Math.max(range.start, range.end > range.start ? range.end - 1 : range.end));
|
|
3167
|
+
const styles = window.getComputedStyle(sourceTextEl);
|
|
3168
|
+
const lineHeightPx = parseFloat(styles.lineHeight) || 18.85;
|
|
3169
|
+
const paddingTop = parseFloat(styles.paddingTop) || 0;
|
|
3170
|
+
const paddingBottom = parseFloat(styles.paddingBottom) || 0;
|
|
3171
|
+
const measureLines = lineNumberMeasureEl ? Array.from(lineNumberMeasureEl.children) : [];
|
|
3172
|
+
|
|
3173
|
+
function getLineTop(lineNumber) {
|
|
3174
|
+
let top = paddingTop;
|
|
3175
|
+
for (let i = 0; i < lineNumber - 1; i += 1) {
|
|
3176
|
+
const lineEl = measureLines[i];
|
|
3177
|
+
top += Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
|
|
3178
|
+
}
|
|
3179
|
+
return top;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function getLineBottom(lineNumber) {
|
|
3183
|
+
const lineEl = measureLines[Math.max(0, lineNumber - 1)];
|
|
3184
|
+
return getLineTop(lineNumber) + Math.max(lineHeightPx, lineEl ? lineEl.getBoundingClientRect().height || 0 : 0);
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
const rangeTop = getLineTop(startLine);
|
|
3188
|
+
const rangeBottom = getLineBottom(endLine);
|
|
3189
|
+
const viewportTop = sourceTextEl.scrollTop;
|
|
3190
|
+
const viewportBottom = viewportTop + sourceTextEl.clientHeight;
|
|
3191
|
+
const margin = Math.max(18, Math.round(sourceTextEl.clientHeight * 0.12));
|
|
3192
|
+
|
|
3193
|
+
let nextScrollTop = viewportTop;
|
|
3194
|
+
if (rangeTop - margin < viewportTop) {
|
|
3195
|
+
nextScrollTop = Math.max(0, rangeTop - margin);
|
|
3196
|
+
} else if (rangeBottom + margin > viewportBottom) {
|
|
3197
|
+
nextScrollTop = Math.max(0, rangeBottom - sourceTextEl.clientHeight + margin + paddingBottom);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
if (Math.abs(nextScrollTop - viewportTop) > 1) {
|
|
3201
|
+
sourceTextEl.scrollTop = nextScrollTop;
|
|
3202
|
+
syncEditorHighlightScroll();
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
2981
3206
|
function scheduleEditorLineNumberRender() {
|
|
2982
3207
|
if (lineNumbersRenderRaf !== null) {
|
|
2983
3208
|
if (typeof window.cancelAnimationFrame === "function") {
|
|
@@ -3025,6 +3250,78 @@
|
|
|
3025
3250
|
return query.get("token") || hash.get("token") || "";
|
|
3026
3251
|
}
|
|
3027
3252
|
|
|
3253
|
+
function buildAuthedStudioUrl(pathname, extraParams) {
|
|
3254
|
+
const token = getToken();
|
|
3255
|
+
if (!token) {
|
|
3256
|
+
throw new Error("Missing Studio token in URL.");
|
|
3257
|
+
}
|
|
3258
|
+
const params = new URLSearchParams(extraParams || {});
|
|
3259
|
+
params.set("token", token);
|
|
3260
|
+
return pathname + "?" + params.toString();
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function updateStudioDocumentUrlState(state) {
|
|
3264
|
+
try {
|
|
3265
|
+
const currentUrl = new URL(window.location.href);
|
|
3266
|
+
const params = currentUrl.searchParams;
|
|
3267
|
+
const nextState = state && typeof state === "object" ? state : sourceState;
|
|
3268
|
+
const nextSource = nextState && nextState.source ? String(nextState.source) : "blank";
|
|
3269
|
+
const nextLabel = nextState && nextState.label ? String(nextState.label) : "blank";
|
|
3270
|
+
const nextPath = nextState && nextState.path ? String(nextState.path) : "";
|
|
3271
|
+
const nextDraftId = nextState && nextState.draftId ? String(nextState.draftId) : "";
|
|
3272
|
+
if (nextSource) params.set("docSource", nextSource);
|
|
3273
|
+
else params.delete("docSource");
|
|
3274
|
+
if (nextLabel) params.set("docLabel", nextLabel);
|
|
3275
|
+
else params.delete("docLabel");
|
|
3276
|
+
if (nextPath) params.set("docPath", nextPath);
|
|
3277
|
+
else params.delete("docPath");
|
|
3278
|
+
if (nextDraftId) params.set("draftId", nextDraftId);
|
|
3279
|
+
else params.delete("draftId");
|
|
3280
|
+
window.history.replaceState(null, "", currentUrl.toString());
|
|
3281
|
+
} catch {
|
|
3282
|
+
// Ignore URL-state update failures.
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
async function fetchStudioJson(pathname, options) {
|
|
3287
|
+
const init = options || {};
|
|
3288
|
+
const headers = new Headers(init.headers || undefined);
|
|
3289
|
+
const method = String(init.method || "GET").toUpperCase();
|
|
3290
|
+
if (init.body != null && !headers.has("Content-Type")) {
|
|
3291
|
+
headers.set("Content-Type", "application/json");
|
|
3292
|
+
}
|
|
3293
|
+
const response = await fetch(buildAuthedStudioUrl(pathname, init.query), {
|
|
3294
|
+
method,
|
|
3295
|
+
headers,
|
|
3296
|
+
body: init.body,
|
|
3297
|
+
cache: "no-store",
|
|
3298
|
+
});
|
|
3299
|
+
let payload = null;
|
|
3300
|
+
try {
|
|
3301
|
+
payload = await response.json();
|
|
3302
|
+
} catch {
|
|
3303
|
+
payload = null;
|
|
3304
|
+
}
|
|
3305
|
+
if (!response.ok || !payload || payload.ok === false) {
|
|
3306
|
+
const message = payload && typeof payload.error === "string"
|
|
3307
|
+
? payload.error
|
|
3308
|
+
: (response.status + " " + response.statusText).trim();
|
|
3309
|
+
throw new Error(message || (method + " " + pathname + " failed."));
|
|
3310
|
+
}
|
|
3311
|
+
return payload;
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
function trySendStudioJsonBeacon(pathname, payload, extraParams) {
|
|
3315
|
+
try {
|
|
3316
|
+
if (!navigator.sendBeacon || typeof navigator.sendBeacon !== "function") return false;
|
|
3317
|
+
const body = JSON.stringify(payload || {});
|
|
3318
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
3319
|
+
return navigator.sendBeacon(buildAuthedStudioUrl(pathname, extraParams), blob);
|
|
3320
|
+
} catch {
|
|
3321
|
+
return false;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3028
3325
|
function makeRequestId() {
|
|
3029
3326
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
3030
3327
|
return window.crypto.randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -3032,6 +3329,10 @@
|
|
|
3032
3329
|
return "req_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
|
|
3033
3330
|
}
|
|
3034
3331
|
|
|
3332
|
+
function makeStudioDraftId() {
|
|
3333
|
+
return "draft_" + makeRequestId();
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3035
3336
|
function escapeHtml(text) {
|
|
3036
3337
|
return text
|
|
3037
3338
|
.replace(/&/g, "&")
|
|
@@ -3549,6 +3850,9 @@
|
|
|
3549
3850
|
sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
|
|
3550
3851
|
sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
|
|
3551
3852
|
}
|
|
3853
|
+
if (reviewNoteGutterEl) {
|
|
3854
|
+
reviewNoteGutterEl.scrollTop = sourceTextEl.scrollTop;
|
|
3855
|
+
}
|
|
3552
3856
|
if (lineNumberGutterEl) {
|
|
3553
3857
|
lineNumberGutterEl.scrollTop = sourceTextEl.scrollTop;
|
|
3554
3858
|
}
|
|
@@ -3628,187 +3932,2081 @@
|
|
|
3628
3932
|
persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
|
|
3629
3933
|
}
|
|
3630
3934
|
|
|
3631
|
-
function readStoredText(storageKey) {
|
|
3632
|
-
if (!window.localStorage) return null;
|
|
3633
|
-
try {
|
|
3634
|
-
const value = window.localStorage.getItem(storageKey);
|
|
3635
|
-
return typeof value === "string" ? value : null;
|
|
3636
|
-
} catch {
|
|
3637
|
-
return null;
|
|
3638
|
-
}
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
function persistStoredText(storageKey, value) {
|
|
3642
|
-
if (!window.localStorage) return;
|
|
3643
|
-
try {
|
|
3644
|
-
window.localStorage.setItem(storageKey, String(value ?? ""));
|
|
3645
|
-
} catch {
|
|
3646
|
-
// ignore storage failures
|
|
3647
|
-
}
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
3935
|
function isScratchpadOpen() {
|
|
3651
3936
|
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
3652
3937
|
}
|
|
3653
3938
|
|
|
3654
|
-
function
|
|
3655
|
-
return
|
|
3939
|
+
function isReviewNotesOpen() {
|
|
3940
|
+
return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
|
|
3656
3941
|
}
|
|
3657
3942
|
|
|
3658
|
-
function
|
|
3659
|
-
|
|
3943
|
+
function syncModalOpenState() {
|
|
3944
|
+
document.body.classList.toggle("scratchpad-open", isScratchpadOpen());
|
|
3660
3945
|
}
|
|
3661
3946
|
|
|
3662
|
-
function
|
|
3663
|
-
const
|
|
3664
|
-
const
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
: "
|
|
3947
|
+
function describeStudioDocument(state) {
|
|
3948
|
+
const currentState = state && typeof state === "object" ? state : sourceState;
|
|
3949
|
+
const source = currentState && currentState.source ? String(currentState.source) : "blank";
|
|
3950
|
+
const label = currentState && currentState.label ? String(currentState.label) : "blank";
|
|
3951
|
+
const path = currentState && currentState.path ? String(currentState.path) : "";
|
|
3952
|
+
const draftId = currentState && currentState.draftId ? String(currentState.draftId) : "";
|
|
3953
|
+
if (path) {
|
|
3954
|
+
return {
|
|
3955
|
+
key: "file:" + path,
|
|
3956
|
+
label: path,
|
|
3957
|
+
fileBacked: true,
|
|
3958
|
+
draftBacked: false,
|
|
3959
|
+
};
|
|
3671
3960
|
}
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
: "
|
|
3961
|
+
const normalizedLabel = label.trim().replace(/\s+/g, " ") || source;
|
|
3962
|
+
if (draftId) {
|
|
3963
|
+
return {
|
|
3964
|
+
key: "draft:" + draftId,
|
|
3965
|
+
label: normalizedLabel,
|
|
3966
|
+
fileBacked: false,
|
|
3967
|
+
draftBacked: true,
|
|
3968
|
+
};
|
|
3676
3969
|
}
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3970
|
+
return {
|
|
3971
|
+
key: "doc:" + source + ":" + normalizedLabel,
|
|
3972
|
+
label: normalizedLabel,
|
|
3973
|
+
fileBacked: false,
|
|
3974
|
+
draftBacked: false,
|
|
3975
|
+
};
|
|
3680
3976
|
}
|
|
3681
3977
|
|
|
3682
|
-
function
|
|
3683
|
-
|
|
3684
|
-
if (scratchpadTextEl && scratchpadTextEl.value !== scratchpadText) {
|
|
3685
|
-
scratchpadTextEl.value = scratchpadText;
|
|
3686
|
-
}
|
|
3687
|
-
if (!options || options.persist !== false) {
|
|
3688
|
-
persistScratchpadText(scratchpadText);
|
|
3689
|
-
}
|
|
3690
|
-
updateScratchpadUi();
|
|
3978
|
+
function getCurrentStudioDocumentDescriptor() {
|
|
3979
|
+
return describeStudioDocument(sourceState);
|
|
3691
3980
|
}
|
|
3692
3981
|
|
|
3693
|
-
function
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3982
|
+
async function fetchScratchpadTextForDocumentKey(documentKey) {
|
|
3983
|
+
const payload = await fetchStudioJson("/scratchpad-state", {
|
|
3984
|
+
query: { documentKey: documentKey },
|
|
3985
|
+
});
|
|
3986
|
+
return payload && typeof payload.text === "string" ? payload.text : "";
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
function flushScratchpadPersistence(documentKeyOverride, textOverride) {
|
|
3990
|
+
const descriptor = documentKeyOverride
|
|
3991
|
+
? { key: String(documentKeyOverride || "").trim() }
|
|
3992
|
+
: getCurrentStudioDocumentDescriptor();
|
|
3993
|
+
const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
|
|
3994
|
+
if (!key) return;
|
|
3995
|
+
if (scratchpadPersistTimer !== null) {
|
|
3996
|
+
window.clearTimeout(scratchpadPersistTimer);
|
|
3997
|
+
scratchpadPersistTimer = null;
|
|
3998
|
+
}
|
|
3999
|
+
const snapshot = String(arguments.length >= 2 ? textOverride : scratchpadText || "");
|
|
4000
|
+
if (trySendStudioJsonBeacon("/scratchpad-state", { documentKey: key, text: snapshot })) {
|
|
4001
|
+
return;
|
|
3706
4002
|
}
|
|
4003
|
+
void fetchStudioJson("/scratchpad-state", {
|
|
4004
|
+
method: "POST",
|
|
4005
|
+
body: JSON.stringify({ documentKey: key, text: snapshot }),
|
|
4006
|
+
}).catch(() => {
|
|
4007
|
+
// Ignore scratchpad persistence failures for now.
|
|
4008
|
+
});
|
|
3707
4009
|
}
|
|
3708
4010
|
|
|
3709
|
-
function
|
|
3710
|
-
if (
|
|
3711
|
-
|
|
3712
|
-
? document.activeElement
|
|
3713
|
-
: sourceTextEl;
|
|
3714
|
-
scratchpadOverlayEl.hidden = false;
|
|
3715
|
-
document.body.classList.add("scratchpad-open");
|
|
3716
|
-
if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
|
|
3717
|
-
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3718
|
-
? window.requestAnimationFrame.bind(window)
|
|
3719
|
-
: (cb) => window.setTimeout(cb, 16);
|
|
3720
|
-
schedule(() => {
|
|
3721
|
-
scratchpadTextEl.focus();
|
|
3722
|
-
if (typeof scratchpadTextEl.selectionStart === "number") {
|
|
3723
|
-
const end = scratchpadTextEl.value.length;
|
|
3724
|
-
scratchpadTextEl.setSelectionRange(end, end);
|
|
3725
|
-
}
|
|
3726
|
-
});
|
|
4011
|
+
function scheduleScratchpadPersistence(text, documentKey) {
|
|
4012
|
+
if (scratchpadPersistTimer !== null) {
|
|
4013
|
+
window.clearTimeout(scratchpadPersistTimer);
|
|
3727
4014
|
}
|
|
4015
|
+
const snapshot = String(text || "");
|
|
4016
|
+
const key = String(documentKey || "").trim();
|
|
4017
|
+
if (!key) return;
|
|
4018
|
+
scratchpadPersistTimer = window.setTimeout(() => {
|
|
4019
|
+
scratchpadPersistTimer = null;
|
|
4020
|
+
flushScratchpadPersistence(key, snapshot);
|
|
4021
|
+
}, 180);
|
|
3728
4022
|
}
|
|
3729
4023
|
|
|
3730
|
-
function
|
|
3731
|
-
const
|
|
3732
|
-
|
|
3733
|
-
|
|
4024
|
+
async function loadScratchpadForDocumentKey(documentKey) {
|
|
4025
|
+
const key = String(documentKey || "").trim();
|
|
4026
|
+
const loadNonce = ++scratchpadLoadNonce;
|
|
4027
|
+
if (!key) {
|
|
4028
|
+
setScratchpadText("", { persist: false });
|
|
3734
4029
|
return;
|
|
3735
4030
|
}
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
setActivePane("left");
|
|
3747
|
-
closeScratchpad({ focusTarget: sourceTextEl });
|
|
3748
|
-
setStatus("Inserted scratchpad into editor.", "success");
|
|
4031
|
+
try {
|
|
4032
|
+
const serverText = await fetchScratchpadTextForDocumentKey(key);
|
|
4033
|
+
if (loadNonce !== scratchpadLoadNonce) return;
|
|
4034
|
+
if (key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4035
|
+
setScratchpadText(serverText, { persist: false });
|
|
4036
|
+
} catch {
|
|
4037
|
+
if (loadNonce !== scratchpadLoadNonce) return;
|
|
4038
|
+
if (key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4039
|
+
setScratchpadText("", { persist: false });
|
|
4040
|
+
}
|
|
3749
4041
|
}
|
|
3750
4042
|
|
|
3751
|
-
function
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
4043
|
+
async function maybeCarryScratchpadToNewDocument(previousDescriptor, nextDescriptor) {
|
|
4044
|
+
if (!previousDescriptor || !nextDescriptor || previousDescriptor.key === nextDescriptor.key) return;
|
|
4045
|
+
const snapshot = String(scratchpadText || "");
|
|
4046
|
+
if (!snapshot.trim()) return;
|
|
4047
|
+
try {
|
|
4048
|
+
const existing = await fetchScratchpadTextForDocumentKey(nextDescriptor.key);
|
|
4049
|
+
if (String(existing || "").trim()) return;
|
|
4050
|
+
await fetchStudioJson("/scratchpad-state", {
|
|
4051
|
+
method: "POST",
|
|
4052
|
+
body: JSON.stringify({ documentKey: nextDescriptor.key, text: snapshot }),
|
|
4053
|
+
});
|
|
4054
|
+
} catch {
|
|
4055
|
+
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
3758
4056
|
}
|
|
4057
|
+
}
|
|
3759
4058
|
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
editorHighlightRenderRaf = null;
|
|
4059
|
+
function loadScratchpadForCurrentDocument(options) {
|
|
4060
|
+
const previousDescriptor = options && options.previousDescriptor ? options.previousDescriptor : null;
|
|
4061
|
+
const shouldCarryToNewDocument = Boolean(options && options.carryCurrentMetadataToNewDocument);
|
|
4062
|
+
const currentDescriptor = getCurrentStudioDocumentDescriptor();
|
|
4063
|
+
void (async () => {
|
|
4064
|
+
if (shouldCarryToNewDocument && previousDescriptor) {
|
|
4065
|
+
await maybeCarryScratchpadToNewDocument(previousDescriptor, currentDescriptor);
|
|
3768
4066
|
}
|
|
4067
|
+
await loadScratchpadForDocumentKey(currentDescriptor.key);
|
|
4068
|
+
})();
|
|
4069
|
+
}
|
|
3769
4070
|
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
}
|
|
3775
|
-
return;
|
|
3776
|
-
}
|
|
4071
|
+
function persistScratchpadText(value) {
|
|
4072
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4073
|
+
scheduleScratchpadPersistence(value, descriptor.key);
|
|
4074
|
+
}
|
|
3777
4075
|
|
|
3778
|
-
|
|
3779
|
-
|
|
4076
|
+
function normalizeReviewNote(note) {
|
|
4077
|
+
if (!note || typeof note !== "object") return null;
|
|
4078
|
+
const id = typeof note.id === "string" && note.id.trim() ? note.id : makeRequestId();
|
|
4079
|
+
const text = typeof note.text === "string" ? note.text : "";
|
|
4080
|
+
const createdAt = typeof note.createdAt === "number" && Number.isFinite(note.createdAt)
|
|
4081
|
+
? note.createdAt
|
|
4082
|
+
: Date.now();
|
|
4083
|
+
const updatedAt = typeof note.updatedAt === "number" && Number.isFinite(note.updatedAt)
|
|
4084
|
+
? note.updatedAt
|
|
4085
|
+
: createdAt;
|
|
4086
|
+
const selectionStart = typeof note.selectionStart === "number" && Number.isFinite(note.selectionStart)
|
|
4087
|
+
? Math.max(0, Math.floor(note.selectionStart))
|
|
4088
|
+
: 0;
|
|
4089
|
+
const selectionEnd = typeof note.selectionEnd === "number" && Number.isFinite(note.selectionEnd)
|
|
4090
|
+
? Math.max(selectionStart, Math.floor(note.selectionEnd))
|
|
4091
|
+
: selectionStart;
|
|
4092
|
+
const lineStart = typeof note.lineStart === "number" && Number.isFinite(note.lineStart)
|
|
4093
|
+
? Math.max(1, Math.floor(note.lineStart))
|
|
4094
|
+
: 1;
|
|
4095
|
+
const lineEnd = typeof note.lineEnd === "number" && Number.isFinite(note.lineEnd)
|
|
4096
|
+
? Math.max(lineStart, Math.floor(note.lineEnd))
|
|
4097
|
+
: lineStart;
|
|
4098
|
+
return {
|
|
4099
|
+
id,
|
|
4100
|
+
text,
|
|
4101
|
+
createdAt,
|
|
4102
|
+
updatedAt,
|
|
4103
|
+
selectionStart,
|
|
4104
|
+
selectionEnd,
|
|
4105
|
+
lineStart,
|
|
4106
|
+
lineEnd,
|
|
4107
|
+
selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
|
|
4108
|
+
selectedDisplayText: typeof note.selectedDisplayText === "string" ? note.selectedDisplayText : "",
|
|
4109
|
+
};
|
|
3780
4110
|
}
|
|
3781
4111
|
|
|
3782
|
-
function
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
4112
|
+
function cloneReviewNotes(notes) {
|
|
4113
|
+
return Array.isArray(notes)
|
|
4114
|
+
? notes
|
|
4115
|
+
.map((note) => normalizeReviewNote(note))
|
|
4116
|
+
.filter(Boolean)
|
|
4117
|
+
.map((note) => ({ ...note }))
|
|
4118
|
+
: [];
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
async function fetchReviewNotesForDocumentKey(documentKey) {
|
|
4122
|
+
const payload = await fetchStudioJson("/review-notes", {
|
|
4123
|
+
query: { documentKey: documentKey },
|
|
4124
|
+
});
|
|
4125
|
+
return cloneReviewNotes(payload && Array.isArray(payload.notes) ? payload.notes : []);
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
function flushReviewNotesPersistence(documentKeyOverride, notesOverride) {
|
|
4129
|
+
const descriptor = documentKeyOverride
|
|
4130
|
+
? { key: String(documentKeyOverride || "").trim() }
|
|
4131
|
+
: getCurrentStudioDocumentDescriptor();
|
|
4132
|
+
const key = String(descriptor && descriptor.key ? descriptor.key : "").trim();
|
|
4133
|
+
if (!key) return;
|
|
4134
|
+
if (reviewNotesPersistTimer !== null) {
|
|
4135
|
+
window.clearTimeout(reviewNotesPersistTimer);
|
|
4136
|
+
reviewNotesPersistTimer = null;
|
|
4137
|
+
}
|
|
4138
|
+
const snapshot = cloneReviewNotes(arguments.length >= 2 ? notesOverride : reviewNotes);
|
|
4139
|
+
if (trySendStudioJsonBeacon("/review-notes", { documentKey: key, notes: snapshot })) {
|
|
3786
4140
|
return;
|
|
3787
4141
|
}
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
:
|
|
4142
|
+
void fetchStudioJson("/review-notes", {
|
|
4143
|
+
method: "POST",
|
|
4144
|
+
body: JSON.stringify({ documentKey: key, notes: snapshot }),
|
|
4145
|
+
}).catch(() => {
|
|
4146
|
+
// Ignore persistence failures; the in-memory notes list remains available for this session.
|
|
4147
|
+
});
|
|
3791
4148
|
}
|
|
3792
4149
|
|
|
3793
|
-
function
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
4150
|
+
function scheduleReviewNotesPersistence() {
|
|
4151
|
+
if (reviewNotesPersistTimer !== null) {
|
|
4152
|
+
window.clearTimeout(reviewNotesPersistTimer);
|
|
4153
|
+
}
|
|
4154
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4155
|
+
const snapshot = cloneReviewNotes(reviewNotes);
|
|
4156
|
+
reviewNotesPersistTimer = window.setTimeout(() => {
|
|
4157
|
+
reviewNotesPersistTimer = null;
|
|
4158
|
+
flushReviewNotesPersistence(descriptor.key, snapshot);
|
|
4159
|
+
}, 180);
|
|
3798
4160
|
}
|
|
3799
4161
|
|
|
3800
|
-
function
|
|
3801
|
-
if (!
|
|
4162
|
+
async function maybeCarryReviewNotesToNewDocument(previousDescriptor, nextDescriptor) {
|
|
4163
|
+
if (!previousDescriptor || !nextDescriptor || previousDescriptor.key === nextDescriptor.key) return;
|
|
4164
|
+
const snapshot = cloneReviewNotes(reviewNotes);
|
|
4165
|
+
if (!snapshot.length) return;
|
|
3802
4166
|
try {
|
|
3803
|
-
const
|
|
3804
|
-
if (
|
|
3805
|
-
|
|
4167
|
+
const existing = await fetchReviewNotesForDocumentKey(nextDescriptor.key);
|
|
4168
|
+
if (existing.length > 0) return;
|
|
4169
|
+
await fetchStudioJson("/review-notes", {
|
|
4170
|
+
method: "POST",
|
|
4171
|
+
body: JSON.stringify({ documentKey: nextDescriptor.key, notes: snapshot }),
|
|
4172
|
+
});
|
|
3806
4173
|
} catch {
|
|
3807
|
-
|
|
4174
|
+
// Ignore carry-over failures and just fall back to normal scope loading.
|
|
3808
4175
|
}
|
|
3809
4176
|
}
|
|
3810
4177
|
|
|
3811
|
-
function
|
|
4178
|
+
async function loadReviewNotesForCurrentDocument(options) {
|
|
4179
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
4180
|
+
const previousDescriptor = options && options.previousDescriptor ? options.previousDescriptor : null;
|
|
4181
|
+
const shouldCarryToNewDocument = Boolean(options && options.carryCurrentMetadataToNewDocument);
|
|
4182
|
+
const loadNonce = ++reviewNotesLoadNonce;
|
|
4183
|
+
try {
|
|
4184
|
+
if (shouldCarryToNewDocument && previousDescriptor) {
|
|
4185
|
+
await maybeCarryReviewNotesToNewDocument(previousDescriptor, descriptor);
|
|
4186
|
+
}
|
|
4187
|
+
const notes = await fetchReviewNotesForDocumentKey(descriptor.key);
|
|
4188
|
+
if (loadNonce !== reviewNotesLoadNonce) return;
|
|
4189
|
+
if (descriptor.key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4190
|
+
reviewNotes = notes;
|
|
4191
|
+
} catch {
|
|
4192
|
+
if (loadNonce !== reviewNotesLoadNonce) return;
|
|
4193
|
+
if (descriptor.key !== getCurrentStudioDocumentDescriptor().key) return;
|
|
4194
|
+
reviewNotes = [];
|
|
4195
|
+
}
|
|
4196
|
+
updateReviewNotesUi();
|
|
4197
|
+
renderReviewNotesList();
|
|
4198
|
+
refreshRenderedEditorPreviewComments();
|
|
4199
|
+
if (editorView === "markdown") {
|
|
4200
|
+
scheduleEditorLineNumberRender();
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
function formatReviewNoteTimestamp(timestamp) {
|
|
4205
|
+
if (!Number.isFinite(timestamp)) return "Saved locally";
|
|
4206
|
+
try {
|
|
4207
|
+
return "Updated " + new Date(timestamp).toLocaleString();
|
|
4208
|
+
} catch {
|
|
4209
|
+
return "Saved locally";
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
function summarizeReviewNoteAnchor(note) {
|
|
4214
|
+
const start = Math.max(1, Number(note && note.lineStart) || 1);
|
|
4215
|
+
const end = Math.max(start, Number(note && note.lineEnd) || start);
|
|
4216
|
+
return start === end ? "Line " + start : ("Lines " + start + "–" + end);
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
function summarizeReviewNoteQuote(note) {
|
|
4220
|
+
const normalized = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
|
|
4221
|
+
.replace(/\s+/g, " ")
|
|
4222
|
+
.trim();
|
|
4223
|
+
if (!normalized) return "Anchor: current line / empty selection";
|
|
4224
|
+
return normalized.length > 140 ? normalized.slice(0, 137) + "…" : normalized;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
function getLineNumberAtOffset(text, offset) {
|
|
4228
|
+
const source = String(text || "");
|
|
4229
|
+
const safeOffset = Math.max(0, Math.min(Number(offset) || 0, source.length));
|
|
4230
|
+
let line = 1;
|
|
4231
|
+
for (let i = 0; i < safeOffset; i += 1) {
|
|
4232
|
+
if (source[i] === "\n") line += 1;
|
|
4233
|
+
}
|
|
4234
|
+
return line;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
function getLineRangeAtOffset(text, offset) {
|
|
4238
|
+
const source = String(text || "");
|
|
4239
|
+
const safeOffset = Math.max(0, Math.min(Number(offset) || 0, source.length));
|
|
4240
|
+
let start = safeOffset;
|
|
4241
|
+
while (start > 0 && source[start - 1] !== "\n") start -= 1;
|
|
4242
|
+
let end = safeOffset;
|
|
4243
|
+
while (end < source.length && source[end] !== "\n") end += 1;
|
|
4244
|
+
return {
|
|
4245
|
+
start,
|
|
4246
|
+
end,
|
|
4247
|
+
lineNumber: getLineNumberAtOffset(source, safeOffset),
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
function getLineRangeForNumbers(text, lineStart, lineEnd) {
|
|
4252
|
+
const lines = String(text || "").split("\n");
|
|
4253
|
+
const safeLineStart = Math.max(1, Math.min(Math.floor(lineStart || 1), Math.max(1, lines.length)));
|
|
4254
|
+
const safeLineEnd = Math.max(safeLineStart, Math.min(Math.floor(lineEnd || safeLineStart), Math.max(1, lines.length)));
|
|
4255
|
+
let start = 0;
|
|
4256
|
+
for (let i = 0; i < safeLineStart - 1; i += 1) {
|
|
4257
|
+
start += lines[i].length + 1;
|
|
4258
|
+
}
|
|
4259
|
+
let end = start;
|
|
4260
|
+
for (let i = safeLineStart - 1; i < safeLineEnd; i += 1) {
|
|
4261
|
+
end += lines[i].length;
|
|
4262
|
+
if (i < safeLineEnd - 1) end += 1;
|
|
4263
|
+
}
|
|
4264
|
+
return { start, end };
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
function getEditorAnchorForReviewNote() {
|
|
4268
|
+
const current = String(sourceTextEl.value || "");
|
|
4269
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
|
|
4270
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
4271
|
+
const safeStart = Math.max(0, Math.min(start, current.length));
|
|
4272
|
+
const safeEnd = Math.max(safeStart, Math.min(end, current.length));
|
|
4273
|
+
if (safeStart !== safeEnd) {
|
|
4274
|
+
return {
|
|
4275
|
+
selectionStart: safeStart,
|
|
4276
|
+
selectionEnd: safeEnd,
|
|
4277
|
+
lineStart: getLineNumberAtOffset(current, safeStart),
|
|
4278
|
+
lineEnd: getLineNumberAtOffset(current, Math.max(safeStart, safeEnd - 1)),
|
|
4279
|
+
selectedText: current.slice(safeStart, safeEnd),
|
|
4280
|
+
selectedDisplayText: current.slice(safeStart, safeEnd),
|
|
4281
|
+
};
|
|
4282
|
+
}
|
|
4283
|
+
const lineRange = getLineRangeAtOffset(current, safeStart);
|
|
4284
|
+
return {
|
|
4285
|
+
selectionStart: lineRange.start,
|
|
4286
|
+
selectionEnd: lineRange.end,
|
|
4287
|
+
lineStart: lineRange.lineNumber,
|
|
4288
|
+
lineEnd: lineRange.lineNumber,
|
|
4289
|
+
selectedText: current.slice(lineRange.start, lineRange.end),
|
|
4290
|
+
selectedDisplayText: current.slice(lineRange.start, lineRange.end),
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
function getEditorLineAnchorForReviewNote() {
|
|
4295
|
+
const current = String(sourceTextEl.value || "");
|
|
4296
|
+
const caret = typeof sourceTextEl.selectionStart === "number"
|
|
4297
|
+
? sourceTextEl.selectionStart
|
|
4298
|
+
: 0;
|
|
4299
|
+
const lineRange = getLineRangeAtOffset(current, Math.max(0, Math.min(caret, current.length)));
|
|
4300
|
+
return {
|
|
4301
|
+
selectionStart: lineRange.start,
|
|
4302
|
+
selectionEnd: lineRange.end,
|
|
4303
|
+
lineStart: lineRange.lineNumber,
|
|
4304
|
+
lineEnd: lineRange.lineNumber,
|
|
4305
|
+
selectedText: current.slice(lineRange.start, lineRange.end),
|
|
4306
|
+
selectedDisplayText: current.slice(lineRange.start, lineRange.end),
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
function resolveReviewNoteRange(note, text) {
|
|
4311
|
+
const source = String(text || "");
|
|
4312
|
+
const safeStart = Math.max(0, Math.min(Number(note && note.selectionStart) || 0, source.length));
|
|
4313
|
+
const safeEnd = Math.max(safeStart, Math.min(Number(note && note.selectionEnd) || safeStart, source.length));
|
|
4314
|
+
const selectedText = String(note && note.selectedText ? note.selectedText : "");
|
|
4315
|
+
if (selectedText && source.slice(safeStart, safeEnd) === selectedText) {
|
|
4316
|
+
return { start: safeStart, end: safeEnd };
|
|
4317
|
+
}
|
|
4318
|
+
if (!selectedText && safeEnd >= safeStart) {
|
|
4319
|
+
return { start: safeStart, end: safeEnd };
|
|
4320
|
+
}
|
|
4321
|
+
if (selectedText) {
|
|
4322
|
+
const foundIndex = source.indexOf(selectedText);
|
|
4323
|
+
if (foundIndex >= 0) {
|
|
4324
|
+
return { start: foundIndex, end: foundIndex + selectedText.length };
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
return getLineRangeForNumbers(source, note && note.lineStart, note && note.lineEnd);
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
function getResolvedReviewNoteLineBounds(note, text) {
|
|
4331
|
+
const source = String(text || "");
|
|
4332
|
+
const range = resolveReviewNoteRange(note, source);
|
|
4333
|
+
if (!range) return null;
|
|
4334
|
+
const startLine = getLineNumberAtOffset(source, range.start);
|
|
4335
|
+
const endLookupOffset = range.end > range.start ? range.end - 1 : range.start;
|
|
4336
|
+
const endLine = getLineNumberAtOffset(source, endLookupOffset);
|
|
4337
|
+
return {
|
|
4338
|
+
start: range.start,
|
|
4339
|
+
end: range.end,
|
|
4340
|
+
lineStart: startLine,
|
|
4341
|
+
lineEnd: Math.max(startLine, endLine),
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
function buildReviewNoteLineMap(text) {
|
|
4346
|
+
const source = String(text || "");
|
|
4347
|
+
const lineMap = new Map();
|
|
4348
|
+
for (const note of reviewNotes) {
|
|
4349
|
+
const bounds = getResolvedReviewNoteLineBounds(note, source);
|
|
4350
|
+
if (!bounds) continue;
|
|
4351
|
+
for (let line = bounds.lineStart; line <= bounds.lineEnd; line += 1) {
|
|
4352
|
+
const notesForLine = lineMap.get(line) || [];
|
|
4353
|
+
notesForLine.push(note);
|
|
4354
|
+
lineMap.set(line, notesForLine);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
return lineMap;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
function supportsPreviewCommentsForCurrentEditor() {
|
|
4361
|
+
return editorLanguage === "markdown";
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
function getPreviewCommentBlockKindLabel(kind) {
|
|
4365
|
+
if (kind === "heading") return "heading";
|
|
4366
|
+
if (kind === "blockquote") return "quote block";
|
|
4367
|
+
if (kind === "list") return "list";
|
|
4368
|
+
if (kind === "code") return "code block";
|
|
4369
|
+
if (kind === "table") return "table";
|
|
4370
|
+
return "paragraph";
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
function supportsPreviewSelectionCommentsForBlockKind(kind) {
|
|
4374
|
+
return kind === "paragraph" || kind === "heading" || kind === "blockquote" || kind === "list";
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
function normalizeVisiblePreviewText(text) {
|
|
4378
|
+
return String(text || "").replace(/\s+/g, " ").trim();
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
|
|
4382
|
+
const safeStart = Math.max(0, Math.min(start, lineText.length));
|
|
4383
|
+
const safeEnd = Math.max(safeStart, Math.min(end, lineText.length));
|
|
4384
|
+
for (let i = safeStart; i < safeEnd; i += 1) {
|
|
4385
|
+
chars.push(lineText[i]);
|
|
4386
|
+
rawOffsets.push(lineBaseOffset + i);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
function buildPreviewSelectionSourceBody(blockText, kind) {
|
|
4391
|
+
const source = String(blockText || "");
|
|
4392
|
+
const lines = source.split("\n");
|
|
4393
|
+
const lineOffsets = [];
|
|
4394
|
+
let runningOffset = 0;
|
|
4395
|
+
for (const line of lines) {
|
|
4396
|
+
lineOffsets.push(runningOffset);
|
|
4397
|
+
runningOffset += line.length + 1;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
const chars = [];
|
|
4401
|
+
const rawOffsets = [];
|
|
4402
|
+
|
|
4403
|
+
function appendLineWithStart(lineIndex, start, end) {
|
|
4404
|
+
const line = lineIndex >= 0 && lineIndex < lines.length ? lines[lineIndex] : "";
|
|
4405
|
+
appendMappedPreviewSlice(chars, rawOffsets, line, lineOffsets[lineIndex] || 0, start, end);
|
|
4406
|
+
if (lineIndex < lines.length - 1) {
|
|
4407
|
+
chars.push("\n");
|
|
4408
|
+
rawOffsets.push((lineOffsets[lineIndex] || 0) + line.length);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4412
|
+
if (kind === "heading") {
|
|
4413
|
+
const firstLine = lines[0] || "";
|
|
4414
|
+
const atxMatch = firstLine.match(/^ {0,3}#{1,6}(?:[ \t]+|$)/);
|
|
4415
|
+
if (atxMatch) {
|
|
4416
|
+
const start = atxMatch[0].length;
|
|
4417
|
+
let end = firstLine.length;
|
|
4418
|
+
const closingMatch = firstLine.slice(start).match(/[ \t]+#+[ \t]*$/);
|
|
4419
|
+
if (closingMatch) {
|
|
4420
|
+
end -= closingMatch[0].length;
|
|
4421
|
+
}
|
|
4422
|
+
appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, start, end);
|
|
4423
|
+
return { text: chars.join(""), rawOffsets };
|
|
4424
|
+
}
|
|
4425
|
+
if (lines.length >= 2 && /^ {0,3}(?:={3,}|-{3,})\s*$/.test(lines[1] || "")) {
|
|
4426
|
+
appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, 0, firstLine.length);
|
|
4427
|
+
return { text: chars.join(""), rawOffsets };
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4432
|
+
const line = lines[lineIndex] || "";
|
|
4433
|
+
if (kind === "blockquote") {
|
|
4434
|
+
const prefixMatch = line.match(/^ {0,3}> ?/);
|
|
4435
|
+
appendLineWithStart(lineIndex, prefixMatch ? prefixMatch[0].length : 0, line.length);
|
|
4436
|
+
continue;
|
|
4437
|
+
}
|
|
4438
|
+
if (kind === "list") {
|
|
4439
|
+
if (!line.trim()) {
|
|
4440
|
+
appendLineWithStart(lineIndex, 0, 0);
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
const itemMatch = line.match(/^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/);
|
|
4444
|
+
if (itemMatch) {
|
|
4445
|
+
appendLineWithStart(lineIndex, itemMatch[0].length, line.length);
|
|
4446
|
+
continue;
|
|
4447
|
+
}
|
|
4448
|
+
const continuationMatch = line.match(/^(?: {1,4}|\t)/);
|
|
4449
|
+
appendLineWithStart(lineIndex, continuationMatch ? continuationMatch[0].length : 0, line.length);
|
|
4450
|
+
continue;
|
|
4451
|
+
}
|
|
4452
|
+
appendLineWithStart(lineIndex, 0, line.length);
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
return { text: chars.join(""), rawOffsets };
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
function buildPreviewInlineDisplayMap(text, rawOffsets) {
|
|
4459
|
+
const source = String(text || "");
|
|
4460
|
+
const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
|
|
4461
|
+
const displayChars = [];
|
|
4462
|
+
const charStarts = [];
|
|
4463
|
+
const charEnds = [];
|
|
4464
|
+
|
|
4465
|
+
function appendChar(character, rawStart, rawEnd) {
|
|
4466
|
+
displayChars.push(character);
|
|
4467
|
+
charStarts.push(rawStart);
|
|
4468
|
+
charEnds.push(rawEnd);
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function appendNestedRange(startIndex, endIndex) {
|
|
4472
|
+
const nested = buildPreviewInlineDisplayMap(
|
|
4473
|
+
source.slice(startIndex, endIndex),
|
|
4474
|
+
rawMap.slice(startIndex, endIndex),
|
|
4475
|
+
);
|
|
4476
|
+
for (let i = 0; i < nested.text.length; i += 1) {
|
|
4477
|
+
appendChar(nested.text[i], nested.charStarts[i], nested.charEnds[i]);
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
let index = 0;
|
|
4482
|
+
while (index < source.length) {
|
|
4483
|
+
const remaining = source.slice(index);
|
|
4484
|
+
const linkMatch = remaining.match(/^!?\[([^\]]*)\]\(([^)]*)\)/);
|
|
4485
|
+
if (linkMatch) {
|
|
4486
|
+
const labelStart = index + (remaining[0] === "!" ? 2 : 1);
|
|
4487
|
+
const labelEnd = labelStart + String(linkMatch[1] || "").length;
|
|
4488
|
+
appendNestedRange(labelStart, labelEnd);
|
|
4489
|
+
index += linkMatch[0].length;
|
|
4490
|
+
continue;
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
if (source[index] === "`") {
|
|
4494
|
+
let tickCount = 1;
|
|
4495
|
+
while (source[index + tickCount] === "`") tickCount += 1;
|
|
4496
|
+
const fence = "`".repeat(tickCount);
|
|
4497
|
+
const closeIndex = source.indexOf(fence, index + tickCount);
|
|
4498
|
+
if (closeIndex >= 0) {
|
|
4499
|
+
for (let i = index + tickCount; i < closeIndex; i += 1) {
|
|
4500
|
+
appendChar(source[i], rawMap[i], rawMap[i] + 1);
|
|
4501
|
+
}
|
|
4502
|
+
index = closeIndex + tickCount;
|
|
4503
|
+
continue;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
if (source[index] === "\\" && index + 1 < source.length) {
|
|
4508
|
+
appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
|
|
4509
|
+
index += 2;
|
|
4510
|
+
continue;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
const htmlTagMatch = remaining.match(/^<\/?[A-Za-z][^>]*>/);
|
|
4514
|
+
if (htmlTagMatch) {
|
|
4515
|
+
index += htmlTagMatch[0].length;
|
|
4516
|
+
continue;
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
const emphasisMatch = remaining.match(/^(?:\*\*\*|\*\*|\*|___|__|_|~~)/);
|
|
4520
|
+
if (emphasisMatch) {
|
|
4521
|
+
index += emphasisMatch[0].length;
|
|
4522
|
+
continue;
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
appendChar(source[index], rawMap[index], rawMap[index] + 1);
|
|
4526
|
+
index += 1;
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
return {
|
|
4530
|
+
text: displayChars.join(""),
|
|
4531
|
+
charStarts,
|
|
4532
|
+
charEnds,
|
|
4533
|
+
};
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
function buildNormalizedPreviewDisplayMap(displayText, charStarts, charEnds) {
|
|
4537
|
+
const source = String(displayText || "");
|
|
4538
|
+
const outChars = [];
|
|
4539
|
+
const outStarts = [];
|
|
4540
|
+
const outEnds = [];
|
|
4541
|
+
let pendingWhitespaceStart = null;
|
|
4542
|
+
let pendingWhitespaceEnd = null;
|
|
4543
|
+
|
|
4544
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
4545
|
+
const character = source[i];
|
|
4546
|
+
if (/\s/.test(character)) {
|
|
4547
|
+
if (outChars.length === 0) continue;
|
|
4548
|
+
if (pendingWhitespaceStart == null) {
|
|
4549
|
+
pendingWhitespaceStart = charStarts[i];
|
|
4550
|
+
}
|
|
4551
|
+
pendingWhitespaceEnd = charEnds[i];
|
|
4552
|
+
continue;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
if (pendingWhitespaceStart != null && pendingWhitespaceEnd != null) {
|
|
4556
|
+
outChars.push(" ");
|
|
4557
|
+
outStarts.push(pendingWhitespaceStart);
|
|
4558
|
+
outEnds.push(pendingWhitespaceEnd);
|
|
4559
|
+
pendingWhitespaceStart = null;
|
|
4560
|
+
pendingWhitespaceEnd = null;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
outChars.push(character);
|
|
4564
|
+
outStarts.push(charStarts[i]);
|
|
4565
|
+
outEnds.push(charEnds[i]);
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
return {
|
|
4569
|
+
text: outChars.join(""),
|
|
4570
|
+
charStarts: outStarts,
|
|
4571
|
+
charEnds: outEnds,
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
function buildNormalizedDomTextMap(rootEl) {
|
|
4576
|
+
if (!rootEl || typeof document.createTreeWalker !== "function") {
|
|
4577
|
+
return { text: "", charStarts: [], charEnds: [] };
|
|
4578
|
+
}
|
|
4579
|
+
const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT);
|
|
4580
|
+
const chars = [];
|
|
4581
|
+
const starts = [];
|
|
4582
|
+
const ends = [];
|
|
4583
|
+
let node = walker.nextNode();
|
|
4584
|
+
while (node) {
|
|
4585
|
+
const textNode = node;
|
|
4586
|
+
const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
|
|
4587
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
4588
|
+
chars.push(value[i]);
|
|
4589
|
+
starts.push({ node: textNode, offset: i });
|
|
4590
|
+
ends.push({ node: textNode, offset: i + 1 });
|
|
4591
|
+
}
|
|
4592
|
+
node = walker.nextNode();
|
|
4593
|
+
}
|
|
4594
|
+
return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
|
|
4595
|
+
}
|
|
4596
|
+
|
|
4597
|
+
function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
|
|
4598
|
+
const source = String(haystack || "");
|
|
4599
|
+
const query = String(needle || "");
|
|
4600
|
+
if (!source || !query) return -1;
|
|
4601
|
+
let bestIndex = -1;
|
|
4602
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
4603
|
+
const desiredIndex = Number.isFinite(preferredIndex) ? Math.max(0, preferredIndex) : 0;
|
|
4604
|
+
for (let matchIndex = source.indexOf(query); matchIndex >= 0; matchIndex = source.indexOf(query, matchIndex + 1)) {
|
|
4605
|
+
const score = Math.abs(matchIndex - desiredIndex);
|
|
4606
|
+
if (score < bestScore) {
|
|
4607
|
+
bestScore = score;
|
|
4608
|
+
bestIndex = matchIndex;
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
return bestIndex;
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
function buildPreviewSelectionDisplayMap(blockText, kind) {
|
|
4615
|
+
const body = buildPreviewSelectionSourceBody(blockText, kind);
|
|
4616
|
+
const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
|
|
4617
|
+
return buildNormalizedPreviewDisplayMap(inlineMap.text, inlineMap.charStarts, inlineMap.charEnds);
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
function getPreviewCommentBlockKey(blockEl) {
|
|
4621
|
+
if (!blockEl || !blockEl.dataset) return "";
|
|
4622
|
+
return [
|
|
4623
|
+
String(blockEl.dataset.reviewNoteStart || ""),
|
|
4624
|
+
String(blockEl.dataset.reviewNoteEnd || ""),
|
|
4625
|
+
String(blockEl.dataset.previewCommentKind || ""),
|
|
4626
|
+
].join(":");
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
function getPreviewCommentSelectionKey(selection) {
|
|
4630
|
+
if (!selection) return "";
|
|
4631
|
+
return [
|
|
4632
|
+
String(selection.blockKey || ""),
|
|
4633
|
+
String(selection.selectionStart || 0),
|
|
4634
|
+
String(selection.selectionEnd || 0),
|
|
4635
|
+
String(selection.selectedDisplayText || ""),
|
|
4636
|
+
].join(":");
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
function setActivePreviewCommentSelection(nextSelection) {
|
|
4640
|
+
const currentKey = getPreviewCommentSelectionKey(activePreviewCommentSelection);
|
|
4641
|
+
const nextKey = getPreviewCommentSelectionKey(nextSelection);
|
|
4642
|
+
if (currentKey === nextKey) return;
|
|
4643
|
+
activePreviewCommentSelection = nextSelection || null;
|
|
4644
|
+
refreshRenderedEditorPreviewComments();
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
function clearPreviewCommentSelection() {
|
|
4648
|
+
setActivePreviewCommentSelection(null);
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
function findPreviewCommentBlockFromNode(node) {
|
|
4652
|
+
if (!node) return null;
|
|
4653
|
+
const element = node instanceof Element ? node : node.parentElement;
|
|
4654
|
+
return element && typeof element.closest === "function"
|
|
4655
|
+
? element.closest(".preview-comment-block")
|
|
4656
|
+
: null;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
function unwrapPreviewJumpHighlightElement(element) {
|
|
4660
|
+
if (!element || !element.parentNode) return;
|
|
4661
|
+
const parent = element.parentNode;
|
|
4662
|
+
while (element.firstChild) {
|
|
4663
|
+
parent.insertBefore(element.firstChild, element);
|
|
4664
|
+
}
|
|
4665
|
+
parent.removeChild(element);
|
|
4666
|
+
if (typeof parent.normalize === "function") {
|
|
4667
|
+
parent.normalize();
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
function clearPreviewJumpHighlight(targetEl) {
|
|
4672
|
+
if (!targetEl) return;
|
|
4673
|
+
const state = previewJumpHighlightState.get(targetEl);
|
|
4674
|
+
if (!state) return;
|
|
4675
|
+
if (state.timer != null) {
|
|
4676
|
+
window.clearTimeout(state.timer);
|
|
4677
|
+
}
|
|
4678
|
+
if (state.inlineHighlightEl) {
|
|
4679
|
+
unwrapPreviewJumpHighlightElement(state.inlineHighlightEl);
|
|
4680
|
+
}
|
|
4681
|
+
if (state.contentEl && state.contentEl.classList) {
|
|
4682
|
+
state.contentEl.classList.remove("preview-jump-highlight");
|
|
4683
|
+
}
|
|
4684
|
+
previewJumpHighlightState.delete(targetEl);
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
function setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl) {
|
|
4688
|
+
if (!targetEl || !contentEl) return;
|
|
4689
|
+
clearPreviewJumpHighlight(targetEl);
|
|
4690
|
+
if (contentEl.classList) {
|
|
4691
|
+
contentEl.classList.add("preview-jump-highlight");
|
|
4692
|
+
}
|
|
4693
|
+
const timer = window.setTimeout(() => {
|
|
4694
|
+
clearPreviewJumpHighlight(targetEl);
|
|
4695
|
+
}, 1800);
|
|
4696
|
+
previewJumpHighlightState.set(targetEl, {
|
|
4697
|
+
contentEl,
|
|
4698
|
+
inlineHighlightEl: inlineHighlightEl || null,
|
|
4699
|
+
timer,
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
function rangesOverlap(startA, endA, startB, endB) {
|
|
4704
|
+
const safeStartA = Math.max(0, Number(startA) || 0);
|
|
4705
|
+
const safeStartB = Math.max(0, Number(startB) || 0);
|
|
4706
|
+
const safeEndA = Math.max(safeStartA + 1, Number(endA) || safeStartA);
|
|
4707
|
+
const safeEndB = Math.max(safeStartB + 1, Number(endB) || safeStartB);
|
|
4708
|
+
return safeStartA < safeEndB && safeStartB < safeEndA;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
function scanMarkdownPreviewCommentBlocks(markdown) {
|
|
4712
|
+
const source = String(markdown || "").replace(/\r\n/g, "\n");
|
|
4713
|
+
const lines = source.split("\n");
|
|
4714
|
+
const lineOffsets = [];
|
|
4715
|
+
let runningOffset = 0;
|
|
4716
|
+
for (const line of lines) {
|
|
4717
|
+
lineOffsets.push(runningOffset);
|
|
4718
|
+
runningOffset += line.length + 1;
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
function getLine(index) {
|
|
4722
|
+
return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
function isBlankLine(index) {
|
|
4726
|
+
return /^\s*$/.test(getLine(index));
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
function lineStartsFence(index) {
|
|
4730
|
+
return getLine(index).match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
function isAtxHeadingLine(index) {
|
|
4734
|
+
return /^ {0,3}#{1,6}(?:[ \t]+|$)/.test(getLine(index));
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
function isSetextUnderlineLine(index) {
|
|
4738
|
+
return /^ {0,3}(?:={3,}|-{3,})\s*$/.test(getLine(index));
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
function isThematicBreakLine(index) {
|
|
4742
|
+
return /^ {0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/.test(getLine(index));
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
function isBlockquoteLine(index) {
|
|
4746
|
+
return /^ {0,3}> ?/.test(getLine(index));
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
function isListLine(index) {
|
|
4750
|
+
return /^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/.test(getLine(index));
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
function isContinuationIndentedLine(index) {
|
|
4754
|
+
return /^(?: {2,}|\t+)/.test(getLine(index));
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
function isPotentialTableRow(index) {
|
|
4758
|
+
const line = getLine(index);
|
|
4759
|
+
return /\|/.test(line) && !/^\s*</.test(line);
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
function isTableDividerLine(index) {
|
|
4763
|
+
return /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/.test(getLine(index));
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
function isHtmlCommentStart(index) {
|
|
4767
|
+
return /^\s*<!--/.test(getLine(index));
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
function makeBlock(kind, startLineIndex, endLineIndex) {
|
|
4771
|
+
const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
|
|
4772
|
+
const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
|
|
4773
|
+
const start = lineOffsets[safeStartLine] || 0;
|
|
4774
|
+
const end = (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
|
|
4775
|
+
return {
|
|
4776
|
+
kind,
|
|
4777
|
+
start,
|
|
4778
|
+
end,
|
|
4779
|
+
lineStart: safeStartLine + 1,
|
|
4780
|
+
lineEnd: safeEndLine + 1,
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
const blocks = [];
|
|
4785
|
+
let index = 0;
|
|
4786
|
+
|
|
4787
|
+
if (/^\s*---\s*$/.test(getLine(0))) {
|
|
4788
|
+
for (let i = 1; i < Math.min(lines.length, 80); i += 1) {
|
|
4789
|
+
if (/^\s*(?:---|\.\.\.)\s*$/.test(getLine(i))) {
|
|
4790
|
+
index = i + 1;
|
|
4791
|
+
break;
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
|
|
4796
|
+
while (index < lines.length) {
|
|
4797
|
+
if (isBlankLine(index)) {
|
|
4798
|
+
index += 1;
|
|
4799
|
+
continue;
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
if (isHtmlCommentStart(index)) {
|
|
4803
|
+
let endComment = index;
|
|
4804
|
+
while (endComment < lines.length && getLine(endComment).indexOf("-->") === -1) {
|
|
4805
|
+
endComment += 1;
|
|
4806
|
+
}
|
|
4807
|
+
index = Math.min(lines.length, endComment + 1);
|
|
4808
|
+
continue;
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
if (isThematicBreakLine(index)) {
|
|
4812
|
+
index += 1;
|
|
4813
|
+
continue;
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4816
|
+
const fenceMatch = lineStartsFence(index);
|
|
4817
|
+
if (fenceMatch) {
|
|
4818
|
+
const marker = fenceMatch[1] || "";
|
|
4819
|
+
const markerChar = marker[0] || "`";
|
|
4820
|
+
const markerLength = marker.length;
|
|
4821
|
+
let endFence = index;
|
|
4822
|
+
for (let i = index + 1; i < lines.length; i += 1) {
|
|
4823
|
+
const closingMatch = getLine(i).match(/^ {0,3}(`{3,}|~{3,})\s*$/);
|
|
4824
|
+
if (closingMatch && closingMatch[1] && closingMatch[1][0] === markerChar && closingMatch[1].length >= markerLength) {
|
|
4825
|
+
endFence = i;
|
|
4826
|
+
break;
|
|
4827
|
+
}
|
|
4828
|
+
endFence = i;
|
|
4829
|
+
}
|
|
4830
|
+
blocks.push(makeBlock("code", index, endFence));
|
|
4831
|
+
index = endFence + 1;
|
|
4832
|
+
continue;
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
if (isAtxHeadingLine(index)) {
|
|
4836
|
+
blocks.push(makeBlock("heading", index, index));
|
|
4837
|
+
index += 1;
|
|
4838
|
+
continue;
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
if (!isBlankLine(index) && index + 1 < lines.length && isSetextUnderlineLine(index + 1)) {
|
|
4842
|
+
blocks.push(makeBlock("heading", index, index + 1));
|
|
4843
|
+
index += 2;
|
|
4844
|
+
continue;
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4847
|
+
if (isPotentialTableRow(index) && index + 1 < lines.length && isTableDividerLine(index + 1)) {
|
|
4848
|
+
let endTable = index + 1;
|
|
4849
|
+
for (let i = index + 2; i < lines.length; i += 1) {
|
|
4850
|
+
if (isBlankLine(i) || !isPotentialTableRow(i)) break;
|
|
4851
|
+
endTable = i;
|
|
4852
|
+
}
|
|
4853
|
+
blocks.push(makeBlock("table", index, endTable));
|
|
4854
|
+
index = endTable + 1;
|
|
4855
|
+
continue;
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
if (isBlockquoteLine(index)) {
|
|
4859
|
+
let endQuote = index;
|
|
4860
|
+
for (let i = index + 1; i < lines.length; i += 1) {
|
|
4861
|
+
if (isBlockquoteLine(i)) {
|
|
4862
|
+
endQuote = i;
|
|
4863
|
+
continue;
|
|
4864
|
+
}
|
|
4865
|
+
if (isBlankLine(i) && i + 1 < lines.length && isBlockquoteLine(i + 1)) {
|
|
4866
|
+
endQuote = i;
|
|
4867
|
+
continue;
|
|
4868
|
+
}
|
|
4869
|
+
break;
|
|
4870
|
+
}
|
|
4871
|
+
blocks.push(makeBlock("blockquote", index, endQuote));
|
|
4872
|
+
index = endQuote + 1;
|
|
4873
|
+
continue;
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
if (isListLine(index)) {
|
|
4877
|
+
let endList = index;
|
|
4878
|
+
for (let i = index + 1; i < lines.length; i += 1) {
|
|
4879
|
+
if (isBlankLine(i)) {
|
|
4880
|
+
if (i + 1 < lines.length && (isListLine(i + 1) || isContinuationIndentedLine(i + 1))) {
|
|
4881
|
+
endList = i;
|
|
4882
|
+
continue;
|
|
4883
|
+
}
|
|
4884
|
+
break;
|
|
4885
|
+
}
|
|
4886
|
+
if (isListLine(i) || isContinuationIndentedLine(i)) {
|
|
4887
|
+
endList = i;
|
|
4888
|
+
continue;
|
|
4889
|
+
}
|
|
4890
|
+
if (isAtxHeadingLine(i) || isBlockquoteLine(i) || lineStartsFence(i) || (isPotentialTableRow(i) && i + 1 < lines.length && isTableDividerLine(i + 1))) {
|
|
4891
|
+
break;
|
|
4892
|
+
}
|
|
4893
|
+
endList = i;
|
|
4894
|
+
}
|
|
4895
|
+
blocks.push(makeBlock("list", index, endList));
|
|
4896
|
+
index = endList + 1;
|
|
4897
|
+
continue;
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
let endParagraph = index;
|
|
4901
|
+
for (let i = index + 1; i < lines.length; i += 1) {
|
|
4902
|
+
if (isBlankLine(i) || isHtmlCommentStart(i) || lineStartsFence(i) || isAtxHeadingLine(i) || isBlockquoteLine(i) || isListLine(i)) {
|
|
4903
|
+
break;
|
|
4904
|
+
}
|
|
4905
|
+
if (i + 1 < lines.length && (isSetextUnderlineLine(i + 1) || (isPotentialTableRow(i) && isTableDividerLine(i + 1)))) {
|
|
4906
|
+
break;
|
|
4907
|
+
}
|
|
4908
|
+
endParagraph = i;
|
|
4909
|
+
}
|
|
4910
|
+
blocks.push(makeBlock("paragraph", index, endParagraph));
|
|
4911
|
+
index = endParagraph + 1;
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
return blocks;
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
function getPreviewCommentTargetKind(element) {
|
|
4918
|
+
if (!element || !(element instanceof Element)) return "";
|
|
4919
|
+
const tag = element.tagName ? element.tagName.toUpperCase() : "";
|
|
4920
|
+
if (/^H[1-6]$/.test(tag)) return "heading";
|
|
4921
|
+
if (tag === "P") return "paragraph";
|
|
4922
|
+
if (tag === "BLOCKQUOTE") return "blockquote";
|
|
4923
|
+
if (tag === "UL" || tag === "OL") return "list";
|
|
4924
|
+
if (tag === "TABLE") return "table";
|
|
4925
|
+
if (tag === "PRE") return "code";
|
|
4926
|
+
if (element.classList) {
|
|
4927
|
+
if (
|
|
4928
|
+
element.classList.contains("sourceCode")
|
|
4929
|
+
|| element.classList.contains("mermaid-container")
|
|
4930
|
+
) {
|
|
4931
|
+
return "code";
|
|
4932
|
+
}
|
|
4933
|
+
if (
|
|
4934
|
+
element.classList.contains("callout-note")
|
|
4935
|
+
|| element.classList.contains("callout-tip")
|
|
4936
|
+
|| element.classList.contains("callout-warning")
|
|
4937
|
+
|| element.classList.contains("callout-important")
|
|
4938
|
+
|| element.classList.contains("callout-caution")
|
|
4939
|
+
) {
|
|
4940
|
+
return "blockquote";
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
return "";
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
function isPreviewCommentTargetElement(element) {
|
|
4947
|
+
return Boolean(getPreviewCommentTargetKind(element));
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
function collectPreviewCommentTargetElements(targetEl) {
|
|
4951
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
|
|
4952
|
+
const selector = "h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, table, div.sourceCode, pre, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
|
|
4953
|
+
return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
|
|
4954
|
+
if (!isPreviewCommentTargetElement(element)) return false;
|
|
4955
|
+
let ancestor = element.parentElement;
|
|
4956
|
+
while (ancestor && ancestor !== targetEl) {
|
|
4957
|
+
if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
|
|
4958
|
+
if (isPreviewCommentTargetElement(ancestor)) return false;
|
|
4959
|
+
ancestor = ancestor.parentElement;
|
|
4960
|
+
}
|
|
4961
|
+
return true;
|
|
4962
|
+
}).map((element) => ({
|
|
4963
|
+
element,
|
|
4964
|
+
kind: getPreviewCommentTargetKind(element),
|
|
4965
|
+
}));
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
|
|
4969
|
+
if (!sourceBlock) return "";
|
|
4970
|
+
const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
|
|
4971
|
+
if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
|
|
4972
|
+
return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
|
|
4973
|
+
}
|
|
4974
|
+
if (sourceBlock.kind === "code") {
|
|
4975
|
+
return normalizeVisiblePreviewText(
|
|
4976
|
+
blockText
|
|
4977
|
+
.replace(/^ {0,3}(`{3,}|~{3,}).*$/gm, "")
|
|
4978
|
+
.replace(/^ {0,3}$/gm, ""),
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
if (sourceBlock.kind === "table") {
|
|
4982
|
+
return normalizeVisiblePreviewText(
|
|
4983
|
+
blockText
|
|
4984
|
+
.replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/gm, "")
|
|
4985
|
+
.replace(/\|/g, " "),
|
|
4986
|
+
);
|
|
4987
|
+
}
|
|
4988
|
+
return normalizeVisiblePreviewText(blockText);
|
|
4989
|
+
}
|
|
4990
|
+
|
|
4991
|
+
function getNormalizedPreviewCommentTargetText(targetEntry) {
|
|
4992
|
+
if (!targetEntry) return "";
|
|
4993
|
+
if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
|
|
4994
|
+
targetEntry.normalizedText = normalizeVisiblePreviewText(
|
|
4995
|
+
targetEntry.element && typeof targetEntry.element.textContent === "string"
|
|
4996
|
+
? targetEntry.element.textContent
|
|
4997
|
+
: "",
|
|
4998
|
+
);
|
|
4999
|
+
return targetEntry.normalizedText;
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
|
|
5003
|
+
const desiredKind = sourceBlock ? sourceBlock.kind : "";
|
|
5004
|
+
const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
|
|
5005
|
+
let fallbackIndex = -1;
|
|
5006
|
+
let containsIndex = -1;
|
|
5007
|
+
|
|
5008
|
+
for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
|
|
5009
|
+
const targetEntry = targetBlocks[i];
|
|
5010
|
+
if (!targetEntry || targetEntry.kind !== desiredKind) continue;
|
|
5011
|
+
if (fallbackIndex < 0) fallbackIndex = i;
|
|
5012
|
+
const targetText = getNormalizedPreviewCommentTargetText(targetEntry);
|
|
5013
|
+
if (desiredText && targetText) {
|
|
5014
|
+
if (targetText === desiredText) {
|
|
5015
|
+
return i;
|
|
5016
|
+
}
|
|
5017
|
+
if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
|
|
5018
|
+
containsIndex = i;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
if (containsIndex >= 0) return containsIndex;
|
|
5024
|
+
return fallbackIndex;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
function getPreviewCommentNotesForRange(start, end, sourceText, displayNotes) {
|
|
5028
|
+
const source = String(sourceText || "");
|
|
5029
|
+
const notes = Array.isArray(displayNotes) ? displayNotes : getDisplayReviewNotes();
|
|
5030
|
+
return notes.filter((note) => {
|
|
5031
|
+
const range = resolveReviewNoteRange(note, source);
|
|
5032
|
+
return range && rangesOverlap(range.start, range.end, start, end);
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
function updatePreviewCommentBlockState(blockEl, sourceText, displayNotes) {
|
|
5037
|
+
if (!blockEl || !blockEl.dataset) return;
|
|
5038
|
+
const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
|
|
5039
|
+
const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
|
|
5040
|
+
const summaryBtn = blockEl.querySelector(".preview-comment-summary");
|
|
5041
|
+
const addBtn = blockEl.querySelector(".preview-comment-add");
|
|
5042
|
+
const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
|
|
5043
|
+
const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
|
|
5044
|
+
const blockKey = getPreviewCommentBlockKey(blockEl);
|
|
5045
|
+
const hasSelection = Boolean(activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey);
|
|
5046
|
+
|
|
5047
|
+
blockEl.classList.remove("has-comments");
|
|
5048
|
+
blockEl.classList.toggle("has-selection", hasSelection);
|
|
5049
|
+
|
|
5050
|
+
if (summaryBtn) {
|
|
5051
|
+
summaryBtn.hidden = true;
|
|
5052
|
+
summaryBtn.textContent = "";
|
|
5053
|
+
summaryBtn.dataset.reviewNoteId = "";
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5056
|
+
if (addBtn) {
|
|
5057
|
+
addBtn.hidden = !hasSelection;
|
|
5058
|
+
addBtn.textContent = "Comment";
|
|
5059
|
+
addBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
|
|
5060
|
+
addBtn.title = hasSelection
|
|
5061
|
+
? ("Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").")
|
|
5062
|
+
: "";
|
|
5063
|
+
addBtn.setAttribute("aria-label", addBtn.title || "Comment");
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
function updatePreviewCommentBlocksForElement(targetEl) {
|
|
5068
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
5069
|
+
const sourceText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5070
|
+
Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
|
|
5071
|
+
updatePreviewCommentBlockState(blockEl, sourceText);
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
|
|
5076
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
5077
|
+
const sourceBlocks = scanMarkdownPreviewCommentBlocks(sourceText);
|
|
5078
|
+
const targetBlocks = collectPreviewCommentTargetElements(targetEl);
|
|
5079
|
+
if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
|
|
5080
|
+
|
|
5081
|
+
let targetIndex = 0;
|
|
5082
|
+
for (const sourceBlock of sourceBlocks) {
|
|
5083
|
+
const matchedTargetIndex = findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, targetIndex);
|
|
5084
|
+
if (matchedTargetIndex < 0) continue;
|
|
5085
|
+
|
|
5086
|
+
const targetEntry = targetBlocks[matchedTargetIndex];
|
|
5087
|
+
targetIndex = matchedTargetIndex + 1;
|
|
5088
|
+
const originalElement = targetEntry && targetEntry.element ? targetEntry.element : null;
|
|
5089
|
+
if (!originalElement || !originalElement.parentNode) continue;
|
|
5090
|
+
|
|
5091
|
+
const wrapper = document.createElement("div");
|
|
5092
|
+
wrapper.className = "preview-comment-block";
|
|
5093
|
+
wrapper.dataset.reviewNoteStart = String(sourceBlock.start);
|
|
5094
|
+
wrapper.dataset.reviewNoteEnd = String(sourceBlock.end);
|
|
5095
|
+
wrapper.dataset.reviewNoteLineStart = String(sourceBlock.lineStart);
|
|
5096
|
+
wrapper.dataset.reviewNoteLineEnd = String(sourceBlock.lineEnd);
|
|
5097
|
+
wrapper.dataset.previewCommentKind = sourceBlock.kind;
|
|
5098
|
+
|
|
5099
|
+
const controls = document.createElement("div");
|
|
5100
|
+
controls.className = "preview-comment-controls";
|
|
5101
|
+
|
|
5102
|
+
const summaryBtn = document.createElement("button");
|
|
5103
|
+
summaryBtn.type = "button";
|
|
5104
|
+
summaryBtn.className = "preview-comment-summary";
|
|
5105
|
+
summaryBtn.hidden = true;
|
|
5106
|
+
controls.appendChild(summaryBtn);
|
|
5107
|
+
|
|
5108
|
+
const addBtn = document.createElement("button");
|
|
5109
|
+
addBtn.type = "button";
|
|
5110
|
+
addBtn.className = "preview-comment-add";
|
|
5111
|
+
addBtn.textContent = "Comment";
|
|
5112
|
+
controls.appendChild(addBtn);
|
|
5113
|
+
|
|
5114
|
+
originalElement.replaceWith(wrapper);
|
|
5115
|
+
wrapper.appendChild(controls);
|
|
5116
|
+
originalElement.classList.add("preview-comment-block-content");
|
|
5117
|
+
wrapper.appendChild(originalElement);
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
updatePreviewCommentBlocksForElement(targetEl);
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
function refreshRenderedEditorPreviewComments() {
|
|
5124
|
+
if (sourcePreviewEl && !sourcePreviewEl.hidden) {
|
|
5125
|
+
updatePreviewCommentBlocksForElement(sourcePreviewEl);
|
|
5126
|
+
}
|
|
5127
|
+
if (critiqueViewEl && rightView === "editor-preview") {
|
|
5128
|
+
updatePreviewCommentBlocksForElement(critiqueViewEl);
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
function buildReviewNoteAnchorFromPreviewBlock(blockEl) {
|
|
5133
|
+
if (!blockEl || !blockEl.dataset) return null;
|
|
5134
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5135
|
+
const selectionStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
|
|
5136
|
+
const selectionEnd = Math.max(selectionStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || selectionStart, source.length));
|
|
5137
|
+
const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
|
|
5138
|
+
const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
|
|
5139
|
+
return {
|
|
5140
|
+
selectionStart,
|
|
5141
|
+
selectionEnd,
|
|
5142
|
+
lineStart,
|
|
5143
|
+
lineEnd,
|
|
5144
|
+
selectedText: source.slice(selectionStart, selectionEnd),
|
|
5145
|
+
selectedDisplayText: source.slice(selectionStart, selectionEnd),
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
function buildReviewNoteAnchorFromPreviewSelection(blockEl, contentEl, range) {
|
|
5150
|
+
if (!blockEl || !blockEl.dataset || !contentEl || !range) return null;
|
|
5151
|
+
const kind = String(blockEl.dataset.previewCommentKind || "");
|
|
5152
|
+
if (!supportsPreviewSelectionCommentsForBlockKind(kind)) return null;
|
|
5153
|
+
if (!contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) return null;
|
|
5154
|
+
|
|
5155
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5156
|
+
const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
|
|
5157
|
+
const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
|
|
5158
|
+
if (blockEnd <= blockStart) return null;
|
|
5159
|
+
|
|
5160
|
+
const sourceBlockText = source.slice(blockStart, blockEnd);
|
|
5161
|
+
const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
|
|
5162
|
+
if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
|
|
5163
|
+
|
|
5164
|
+
const prefixRange = document.createRange();
|
|
5165
|
+
prefixRange.selectNodeContents(contentEl);
|
|
5166
|
+
prefixRange.setEnd(range.startContainer, range.startOffset);
|
|
5167
|
+
const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
|
|
5168
|
+
const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
|
|
5169
|
+
if (!selectedDisplayText) return null;
|
|
5170
|
+
|
|
5171
|
+
const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
|
|
5172
|
+
const bestIndex = findPreferredNormalizedTextMatch(displayMap.text, selectedDisplayText, desiredStart);
|
|
5173
|
+
if (bestIndex < 0) return null;
|
|
5174
|
+
|
|
5175
|
+
const endIndex = bestIndex + selectedDisplayText.length - 1;
|
|
5176
|
+
const rawStartRel = displayMap.charStarts[bestIndex];
|
|
5177
|
+
const rawEndRel = displayMap.charEnds[endIndex];
|
|
5178
|
+
if (!Number.isFinite(rawStartRel) || !Number.isFinite(rawEndRel) || rawEndRel <= rawStartRel) {
|
|
5179
|
+
return null;
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
const selectionStart = blockStart + rawStartRel;
|
|
5183
|
+
const selectionEnd = blockStart + rawEndRel;
|
|
5184
|
+
return {
|
|
5185
|
+
selectionStart,
|
|
5186
|
+
selectionEnd,
|
|
5187
|
+
lineStart: getLineNumberAtOffset(source, selectionStart),
|
|
5188
|
+
lineEnd: getLineNumberAtOffset(source, Math.max(selectionStart, selectionEnd - 1)),
|
|
5189
|
+
selectedText: source.slice(selectionStart, selectionEnd),
|
|
5190
|
+
selectedDisplayText,
|
|
5191
|
+
};
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
function getPreviewJumpNormalizedSelectionStart(note, blockEl, range) {
|
|
5195
|
+
if (!note || !blockEl || !blockEl.dataset || !range) return 0;
|
|
5196
|
+
const kind = String(blockEl.dataset.previewCommentKind || "");
|
|
5197
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5198
|
+
const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
|
|
5199
|
+
const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
|
|
5200
|
+
const displayMap = buildPreviewSelectionDisplayMap(source.slice(blockStart, blockEnd), kind);
|
|
5201
|
+
if (!displayMap || !displayMap.charStarts || displayMap.charStarts.length === 0) return 0;
|
|
5202
|
+
const relativeStart = Math.max(0, range.start - blockStart);
|
|
5203
|
+
for (let i = 0; i < displayMap.charStarts.length; i += 1) {
|
|
5204
|
+
const charStart = Number(displayMap.charStarts[i]);
|
|
5205
|
+
const charEnd = Number(displayMap.charEnds[i]);
|
|
5206
|
+
if (charEnd > relativeStart && charStart <= relativeStart) {
|
|
5207
|
+
return i;
|
|
5208
|
+
}
|
|
5209
|
+
if (charStart >= relativeStart) {
|
|
5210
|
+
return i;
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
return Math.max(0, displayMap.text.length - 1);
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
function createPreviewJumpInlineHighlight(contentEl, blockEl, note, range) {
|
|
5217
|
+
if (!contentEl || !note || !range) return null;
|
|
5218
|
+
const selectedDisplayText = normalizeVisiblePreviewText(note.selectedDisplayText || note.selectedText || "");
|
|
5219
|
+
if (!selectedDisplayText) return null;
|
|
5220
|
+
const domMap = buildNormalizedDomTextMap(contentEl);
|
|
5221
|
+
if (!domMap.text || !domMap.charStarts.length || !domMap.charEnds.length) return null;
|
|
5222
|
+
const preferredStart = getPreviewJumpNormalizedSelectionStart(note, blockEl, range);
|
|
5223
|
+
const matchIndex = findPreferredNormalizedTextMatch(domMap.text, selectedDisplayText, preferredStart);
|
|
5224
|
+
if (matchIndex < 0) return null;
|
|
5225
|
+
const endIndex = matchIndex + selectedDisplayText.length - 1;
|
|
5226
|
+
const startRef = domMap.charStarts[matchIndex];
|
|
5227
|
+
const endRef = domMap.charEnds[endIndex];
|
|
5228
|
+
if (!startRef || !endRef || !startRef.node || !endRef.node) return null;
|
|
5229
|
+
|
|
5230
|
+
const domRange = document.createRange();
|
|
5231
|
+
domRange.setStart(startRef.node, startRef.offset);
|
|
5232
|
+
domRange.setEnd(endRef.node, endRef.offset);
|
|
5233
|
+
|
|
5234
|
+
const highlightEl = document.createElement("span");
|
|
5235
|
+
highlightEl.className = "preview-comment-inline-highlight";
|
|
5236
|
+
try {
|
|
5237
|
+
domRange.surroundContents(highlightEl);
|
|
5238
|
+
} catch {
|
|
5239
|
+
const fragment = domRange.extractContents();
|
|
5240
|
+
highlightEl.appendChild(fragment);
|
|
5241
|
+
domRange.insertNode(highlightEl);
|
|
5242
|
+
}
|
|
5243
|
+
return highlightEl;
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
function findPreviewCommentBlockForRange(targetEl, range) {
|
|
5247
|
+
if (!targetEl || !range || typeof targetEl.querySelectorAll !== "function") return null;
|
|
5248
|
+
let bestBlock = null;
|
|
5249
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
5250
|
+
Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
|
|
5251
|
+
const blockStart = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
|
|
5252
|
+
const blockEnd = Math.max(blockStart, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || blockStart);
|
|
5253
|
+
const overlapStart = Math.max(blockStart, range.start);
|
|
5254
|
+
const overlapEnd = Math.min(blockEnd, range.end);
|
|
5255
|
+
const overlap = Math.max(0, overlapEnd - overlapStart);
|
|
5256
|
+
const contains = range.start >= blockStart && range.end <= blockEnd;
|
|
5257
|
+
const distance = contains
|
|
5258
|
+
? 0
|
|
5259
|
+
: Math.min(Math.abs(range.start - blockEnd), Math.abs(range.end - blockStart));
|
|
5260
|
+
const score = contains
|
|
5261
|
+
? (1000000 - (blockEnd - blockStart))
|
|
5262
|
+
: (overlap > 0 ? overlap : -distance);
|
|
5263
|
+
if (score > bestScore) {
|
|
5264
|
+
bestScore = score;
|
|
5265
|
+
bestBlock = blockEl;
|
|
5266
|
+
}
|
|
5267
|
+
});
|
|
5268
|
+
return bestBlock;
|
|
5269
|
+
}
|
|
5270
|
+
|
|
5271
|
+
function revealReviewNoteInPreviewElement(targetEl, note) {
|
|
5272
|
+
if (!targetEl || !note) return false;
|
|
5273
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5274
|
+
const range = resolveReviewNoteRange(note, source);
|
|
5275
|
+
if (!range) return false;
|
|
5276
|
+
const blockEl = findPreviewCommentBlockForRange(targetEl, range);
|
|
5277
|
+
if (!blockEl) return false;
|
|
5278
|
+
const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
|
|
5279
|
+
const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
|
|
5280
|
+
if (typeof blockEl.scrollIntoView === "function") {
|
|
5281
|
+
blockEl.scrollIntoView({ block: "center", inline: "nearest" });
|
|
5282
|
+
}
|
|
5283
|
+
setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl);
|
|
5284
|
+
return true;
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
function revealReviewNoteInPreview(note) {
|
|
5288
|
+
if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
|
|
5289
|
+
revealReviewNoteInPreviewElement(critiqueViewEl, note);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
function updateActivePreviewCommentSelectionFromDom() {
|
|
5294
|
+
const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
5295
|
+
if (!selection || selection.rangeCount <= 0 || selection.isCollapsed) {
|
|
5296
|
+
clearPreviewCommentSelection();
|
|
5297
|
+
return;
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
const range = selection.getRangeAt(0);
|
|
5301
|
+
const startBlock = findPreviewCommentBlockFromNode(range.startContainer);
|
|
5302
|
+
const endBlock = findPreviewCommentBlockFromNode(range.endContainer);
|
|
5303
|
+
if (!startBlock || !endBlock || startBlock !== endBlock) {
|
|
5304
|
+
clearPreviewCommentSelection();
|
|
5305
|
+
return;
|
|
5306
|
+
}
|
|
5307
|
+
|
|
5308
|
+
const contentEl = startBlock.querySelector(".preview-comment-block-content");
|
|
5309
|
+
if (!contentEl || !contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) {
|
|
5310
|
+
clearPreviewCommentSelection();
|
|
5311
|
+
return;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
const anchor = buildReviewNoteAnchorFromPreviewSelection(startBlock, contentEl, range);
|
|
5315
|
+
if (!anchor) {
|
|
5316
|
+
clearPreviewCommentSelection();
|
|
5317
|
+
return;
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5320
|
+
setActivePreviewCommentSelection({
|
|
5321
|
+
...anchor,
|
|
5322
|
+
blockKey: getPreviewCommentBlockKey(startBlock),
|
|
5323
|
+
});
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
function getDisplayReviewNotes() {
|
|
5327
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5328
|
+
return reviewNotes.slice().sort((left, right) => {
|
|
5329
|
+
const leftBounds = getResolvedReviewNoteLineBounds(left, source);
|
|
5330
|
+
const rightBounds = getResolvedReviewNoteLineBounds(right, source);
|
|
5331
|
+
const leftLine = leftBounds ? leftBounds.lineStart : Math.max(1, Number(left && left.lineStart) || 1);
|
|
5332
|
+
const rightLine = rightBounds ? rightBounds.lineStart : Math.max(1, Number(right && right.lineStart) || 1);
|
|
5333
|
+
if (leftLine !== rightLine) return leftLine - rightLine;
|
|
5334
|
+
|
|
5335
|
+
const leftStart = leftBounds ? leftBounds.start : Math.max(0, Number(left && left.selectionStart) || 0);
|
|
5336
|
+
const rightStart = rightBounds ? rightBounds.start : Math.max(0, Number(right && right.selectionStart) || 0);
|
|
5337
|
+
if (leftStart !== rightStart) return leftStart - rightStart;
|
|
5338
|
+
|
|
5339
|
+
const leftCreated = Number(left && left.createdAt) || 0;
|
|
5340
|
+
const rightCreated = Number(right && right.createdAt) || 0;
|
|
5341
|
+
if (leftCreated !== rightCreated) return leftCreated - rightCreated;
|
|
5342
|
+
|
|
5343
|
+
return String(left && left.id ? left.id : "").localeCompare(String(right && right.id ? right.id : ""));
|
|
5344
|
+
});
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
function focusReviewNoteInPanel(noteId) {
|
|
5348
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
5349
|
+
if (!note) return;
|
|
5350
|
+
pendingReviewNoteFocusId = note.id;
|
|
5351
|
+
openReviewNotes();
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5354
|
+
function escapeReviewNoteAnnotationText(text) {
|
|
5355
|
+
return String(text || "")
|
|
5356
|
+
.replace(/\\/g, "\\\\")
|
|
5357
|
+
.replace(/\]/g, "\\]")
|
|
5358
|
+
.trim();
|
|
5359
|
+
}
|
|
5360
|
+
|
|
5361
|
+
function getReviewNoteInlineState(note, text) {
|
|
5362
|
+
const source = String(text || "");
|
|
5363
|
+
const annotationBody = escapeReviewNoteAnnotationText(note && note.text);
|
|
5364
|
+
if (!annotationBody) {
|
|
5365
|
+
return {
|
|
5366
|
+
annotationBody: "",
|
|
5367
|
+
range: null,
|
|
5368
|
+
markerText: "",
|
|
5369
|
+
exists: false,
|
|
5370
|
+
canToggle: false,
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
5373
|
+
const range = resolveReviewNoteRange(note, source);
|
|
5374
|
+
if (!range) {
|
|
5375
|
+
return {
|
|
5376
|
+
annotationBody,
|
|
5377
|
+
range: null,
|
|
5378
|
+
markerText: "",
|
|
5379
|
+
exists: false,
|
|
5380
|
+
canToggle: false,
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
const markerText = (range.start === range.end ? "" : " ") + "[an: " + annotationBody + "]";
|
|
5384
|
+
const exists = source.slice(range.end, range.end + markerText.length) === markerText;
|
|
5385
|
+
return {
|
|
5386
|
+
annotationBody,
|
|
5387
|
+
range,
|
|
5388
|
+
markerText,
|
|
5389
|
+
exists,
|
|
5390
|
+
canToggle: true,
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
function setReviewNotes(nextNotes, options) {
|
|
5395
|
+
reviewNotes = cloneReviewNotes(nextNotes);
|
|
5396
|
+
updateReviewNotesUi();
|
|
5397
|
+
renderReviewNotesList();
|
|
5398
|
+
refreshRenderedEditorPreviewComments();
|
|
5399
|
+
if (editorView === "markdown") {
|
|
5400
|
+
scheduleEditorLineNumberRender();
|
|
5401
|
+
}
|
|
5402
|
+
if (!options || options.persist !== false) {
|
|
5403
|
+
scheduleReviewNotesPersistence();
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
function updateEditorSelectionCommentUi() {
|
|
5408
|
+
if (!editorSelectionCommentBtn) return;
|
|
5409
|
+
const hasSelection = Boolean(
|
|
5410
|
+
editorView === "markdown"
|
|
5411
|
+
&& document.activeElement === sourceTextEl
|
|
5412
|
+
&& typeof sourceTextEl.selectionStart === "number"
|
|
5413
|
+
&& typeof sourceTextEl.selectionEnd === "number"
|
|
5414
|
+
&& sourceTextEl.selectionEnd > sourceTextEl.selectionStart
|
|
5415
|
+
);
|
|
5416
|
+
editorSelectionCommentBtn.hidden = !hasSelection;
|
|
5417
|
+
if (hasSelection) {
|
|
5418
|
+
editorSelectionCommentBtn.title = "Create a new local comment from the current editor selection.";
|
|
5419
|
+
editorSelectionCommentBtn.setAttribute("aria-label", editorSelectionCommentBtn.title);
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
function updateReviewNotesUi() {
|
|
5424
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
5425
|
+
const count = reviewNotes.length;
|
|
5426
|
+
const hasNotes = count > 0;
|
|
5427
|
+
const isOpen = isReviewNotesOpen();
|
|
5428
|
+
if (reviewNotesBtn) {
|
|
5429
|
+
reviewNotesBtn.textContent = hasNotes ? "Comments •" : "Comments";
|
|
5430
|
+
reviewNotesBtn.classList.toggle("has-content", hasNotes);
|
|
5431
|
+
reviewNotesBtn.classList.toggle("is-active", isOpen);
|
|
5432
|
+
reviewNotesBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
|
|
5433
|
+
reviewNotesBtn.title = isOpen
|
|
5434
|
+
? "Hide local comments."
|
|
5435
|
+
: (hasNotes
|
|
5436
|
+
? (count + " local comment" + (count === 1 ? "" : "s") + " for " + descriptor.label + ". Open the side-by-side comments rail.")
|
|
5437
|
+
: "Open local comments beside the current editor document or draft. Comments stay outside the document text and can later be converted into [an: ...] annotations.");
|
|
5438
|
+
}
|
|
5439
|
+
if (reviewNotesMetaEl) {
|
|
5440
|
+
const scopeLabel = descriptor.fileBacked
|
|
5441
|
+
? "file-backed"
|
|
5442
|
+
: (descriptor.draftBacked ? "draft-backed" : "local buffer");
|
|
5443
|
+
reviewNotesMetaEl.textContent = hasNotes
|
|
5444
|
+
? (count + " comment" + (count === 1 ? "" : "s") + " · " + scopeLabel + " · " + descriptor.label)
|
|
5445
|
+
: ("No comments yet · " + scopeLabel);
|
|
5446
|
+
}
|
|
5447
|
+
if (reviewNotesAddBtn) {
|
|
5448
|
+
reviewNotesAddBtn.disabled = editorView !== "markdown";
|
|
5449
|
+
reviewNotesAddBtn.title = editorView === "markdown"
|
|
5450
|
+
? "Create a new local comment on the current editor line."
|
|
5451
|
+
: (supportsPreviewCommentsForCurrentEditor()
|
|
5452
|
+
? "Select preview text and use Comment for a local preview-anchored comment."
|
|
5453
|
+
: "Switch to Editor (Raw) to comment on the current line.");
|
|
5454
|
+
}
|
|
5455
|
+
if (reviewNotesInlineAllBtn) {
|
|
5456
|
+
const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5457
|
+
const toggleCandidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, currentText).canToggle);
|
|
5458
|
+
const allInline = toggleCandidates.length > 0 && toggleCandidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
|
|
5459
|
+
reviewNotesInlineAllBtn.disabled = uiBusy || toggleCandidates.length === 0;
|
|
5460
|
+
reviewNotesInlineAllBtn.textContent = allInline ? "All inline: On" : "All inline: Off";
|
|
5461
|
+
reviewNotesInlineAllBtn.setAttribute("aria-pressed", allInline ? "true" : "false");
|
|
5462
|
+
reviewNotesInlineAllBtn.title = allInline
|
|
5463
|
+
? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
|
|
5464
|
+
: "Inline annotations derived from all non-empty comments are currently off. Click to add them.";
|
|
5465
|
+
}
|
|
5466
|
+
if (reviewNotesDoneBtn) {
|
|
5467
|
+
reviewNotesDoneBtn.disabled = !isOpen;
|
|
5468
|
+
}
|
|
5469
|
+
if (reviewNotesEmptyStateEl) {
|
|
5470
|
+
reviewNotesEmptyStateEl.hidden = hasNotes;
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
function renderReviewNotesList() {
|
|
5475
|
+
if (!reviewNotesListEl) return;
|
|
5476
|
+
reviewNotesListEl.innerHTML = "";
|
|
5477
|
+
for (const note of getDisplayReviewNotes()) {
|
|
5478
|
+
const card = document.createElement("article");
|
|
5479
|
+
card.className = "review-note-card";
|
|
5480
|
+
|
|
5481
|
+
const header = document.createElement("div");
|
|
5482
|
+
header.className = "review-note-card-header";
|
|
5483
|
+
|
|
5484
|
+
const titleWrap = document.createElement("div");
|
|
5485
|
+
titleWrap.className = "review-note-card-title";
|
|
5486
|
+
|
|
5487
|
+
const anchor = document.createElement("span");
|
|
5488
|
+
anchor.className = "review-note-anchor";
|
|
5489
|
+
anchor.textContent = summarizeReviewNoteAnchor(note);
|
|
5490
|
+
titleWrap.appendChild(anchor);
|
|
5491
|
+
|
|
5492
|
+
const quote = document.createElement("div");
|
|
5493
|
+
quote.className = "review-note-quote";
|
|
5494
|
+
quote.textContent = summarizeReviewNoteQuote(note);
|
|
5495
|
+
titleWrap.appendChild(quote);
|
|
5496
|
+
header.appendChild(titleWrap);
|
|
5497
|
+
|
|
5498
|
+
card.appendChild(header);
|
|
5499
|
+
|
|
5500
|
+
const textarea = document.createElement("textarea");
|
|
5501
|
+
textarea.value = String(note.text || "");
|
|
5502
|
+
textarea.placeholder = "Write a local comment here…";
|
|
5503
|
+
textarea.title = "Write a local comment. Press Enter to finish editing, or Shift+Enter for a new line.";
|
|
5504
|
+
card.appendChild(textarea);
|
|
5505
|
+
|
|
5506
|
+
const footer = document.createElement("div");
|
|
5507
|
+
footer.className = "review-note-card-footer";
|
|
5508
|
+
|
|
5509
|
+
const timestamp = document.createElement("span");
|
|
5510
|
+
timestamp.className = "review-note-timestamp";
|
|
5511
|
+
timestamp.textContent = formatReviewNoteTimestamp(note.updatedAt);
|
|
5512
|
+
|
|
5513
|
+
const actions = document.createElement("div");
|
|
5514
|
+
actions.className = "review-note-card-actions";
|
|
5515
|
+
|
|
5516
|
+
const jumpBtn = document.createElement("button");
|
|
5517
|
+
jumpBtn.type = "button";
|
|
5518
|
+
jumpBtn.textContent = "Jump";
|
|
5519
|
+
jumpBtn.title = "Jump to this comment's anchored location in the editor.";
|
|
5520
|
+
jumpBtn.addEventListener("click", () => {
|
|
5521
|
+
jumpToReviewNote(note.id);
|
|
5522
|
+
});
|
|
5523
|
+
actions.appendChild(jumpBtn);
|
|
5524
|
+
|
|
5525
|
+
const inlineState = getReviewNoteInlineState(note, sourceTextEl.value || "");
|
|
5526
|
+
const convertBtn = document.createElement("button");
|
|
5527
|
+
convertBtn.type = "button";
|
|
5528
|
+
convertBtn.className = "review-note-inline-btn";
|
|
5529
|
+
convertBtn.textContent = inlineState.exists ? "Inline: On" : "Inline: Off";
|
|
5530
|
+
convertBtn.setAttribute("aria-pressed", inlineState.exists ? "true" : "false");
|
|
5531
|
+
convertBtn.disabled = !inlineState.canToggle || uiBusy;
|
|
5532
|
+
convertBtn.title = inlineState.exists
|
|
5533
|
+
? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
|
|
5534
|
+
: "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
|
|
5535
|
+
convertBtn.addEventListener("click", () => {
|
|
5536
|
+
convertReviewNoteToAnnotation(note.id);
|
|
5537
|
+
});
|
|
5538
|
+
actions.appendChild(convertBtn);
|
|
5539
|
+
|
|
5540
|
+
const deleteBtn = document.createElement("button");
|
|
5541
|
+
deleteBtn.type = "button";
|
|
5542
|
+
deleteBtn.className = "review-note-delete-btn";
|
|
5543
|
+
deleteBtn.textContent = "Delete";
|
|
5544
|
+
deleteBtn.title = "Delete this local comment.";
|
|
5545
|
+
deleteBtn.addEventListener("click", () => {
|
|
5546
|
+
deleteReviewNote(note.id);
|
|
5547
|
+
});
|
|
5548
|
+
actions.appendChild(deleteBtn);
|
|
5549
|
+
|
|
5550
|
+
footer.appendChild(timestamp);
|
|
5551
|
+
footer.appendChild(actions);
|
|
5552
|
+
card.appendChild(footer);
|
|
5553
|
+
|
|
5554
|
+
textarea.addEventListener("input", () => {
|
|
5555
|
+
note.text = textarea.value;
|
|
5556
|
+
note.updatedAt = Date.now();
|
|
5557
|
+
timestamp.textContent = formatReviewNoteTimestamp(note.updatedAt);
|
|
5558
|
+
const nextInlineState = getReviewNoteInlineState(note, sourceTextEl.value || "");
|
|
5559
|
+
convertBtn.disabled = !nextInlineState.canToggle || uiBusy;
|
|
5560
|
+
convertBtn.textContent = nextInlineState.exists ? "Inline: On" : "Inline: Off";
|
|
5561
|
+
convertBtn.setAttribute("aria-pressed", nextInlineState.exists ? "true" : "false");
|
|
5562
|
+
convertBtn.title = nextInlineState.exists
|
|
5563
|
+
? "This comment currently has an inline [an: ...] annotation in the editor. Click to remove it."
|
|
5564
|
+
: "This comment is currently not inline in the editor. Click to add it as an inline [an: ...] annotation.";
|
|
5565
|
+
scheduleReviewNotesPersistence();
|
|
5566
|
+
updateReviewNotesUi();
|
|
5567
|
+
});
|
|
5568
|
+
|
|
5569
|
+
textarea.addEventListener("keydown", (event) => {
|
|
5570
|
+
if (
|
|
5571
|
+
event.key === "Enter"
|
|
5572
|
+
&& !event.shiftKey
|
|
5573
|
+
&& !event.altKey
|
|
5574
|
+
&& !event.ctrlKey
|
|
5575
|
+
&& !event.metaKey
|
|
5576
|
+
) {
|
|
5577
|
+
event.preventDefault();
|
|
5578
|
+
textarea.blur();
|
|
5579
|
+
if (!convertBtn.disabled) {
|
|
5580
|
+
convertBtn.focus();
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
});
|
|
5584
|
+
|
|
5585
|
+
reviewNotesListEl.appendChild(card);
|
|
5586
|
+
|
|
5587
|
+
if (pendingReviewNoteInlineFocusId && pendingReviewNoteInlineFocusId === note.id && isReviewNotesOpen()) {
|
|
5588
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5589
|
+
? window.requestAnimationFrame.bind(window)
|
|
5590
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5591
|
+
schedule(() => {
|
|
5592
|
+
card.scrollIntoView({ block: "nearest" });
|
|
5593
|
+
if (!convertBtn.disabled) convertBtn.focus();
|
|
5594
|
+
});
|
|
5595
|
+
} else if (pendingReviewNoteFocusId && pendingReviewNoteFocusId === note.id && isReviewNotesOpen()) {
|
|
5596
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5597
|
+
? window.requestAnimationFrame.bind(window)
|
|
5598
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5599
|
+
schedule(() => {
|
|
5600
|
+
card.scrollIntoView({ block: "nearest" });
|
|
5601
|
+
textarea.focus();
|
|
5602
|
+
const end = textarea.value.length;
|
|
5603
|
+
textarea.setSelectionRange(end, end);
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
pendingReviewNoteFocusId = null;
|
|
5608
|
+
pendingReviewNoteInlineFocusId = null;
|
|
5609
|
+
}
|
|
5610
|
+
|
|
5611
|
+
function focusReviewNotesForPreviewBlock(blockEl) {
|
|
5612
|
+
if (!blockEl) return;
|
|
5613
|
+
const start = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
|
|
5614
|
+
const end = Math.max(start, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || start);
|
|
5615
|
+
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5616
|
+
const notes = getPreviewCommentNotesForRange(start, end, source);
|
|
5617
|
+
if (!notes.length) return;
|
|
5618
|
+
focusReviewNoteInPanel(notes[0].id);
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
function addReviewNoteFromPreviewBlock(blockEl) {
|
|
5622
|
+
const anchor = buildReviewNoteAnchorFromPreviewBlock(blockEl);
|
|
5623
|
+
if (!anchor) return null;
|
|
5624
|
+
return addReviewNoteFromAnchor(anchor, {
|
|
5625
|
+
statusMessage: "Added local comment from editor preview.",
|
|
5626
|
+
});
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
function addReviewNoteFromPreviewSelection(blockEl) {
|
|
5630
|
+
if (!blockEl) return null;
|
|
5631
|
+
const blockKey = getPreviewCommentBlockKey(blockEl);
|
|
5632
|
+
const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
|
|
5633
|
+
? activePreviewCommentSelection
|
|
5634
|
+
: null;
|
|
5635
|
+
if (!anchor) {
|
|
5636
|
+
setStatus("Select some preview text within a single block first.", "warning");
|
|
5637
|
+
return null;
|
|
5638
|
+
}
|
|
5639
|
+
const note = addReviewNoteFromAnchor(anchor, {
|
|
5640
|
+
statusMessage: "Added local comment from preview selection.",
|
|
5641
|
+
});
|
|
5642
|
+
if (note) {
|
|
5643
|
+
const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
5644
|
+
if (selection && typeof selection.removeAllRanges === "function") {
|
|
5645
|
+
selection.removeAllRanges();
|
|
5646
|
+
}
|
|
5647
|
+
clearPreviewCommentSelection();
|
|
5648
|
+
}
|
|
5649
|
+
return note;
|
|
5650
|
+
}
|
|
5651
|
+
|
|
5652
|
+
function addReviewNoteFromAnchor(anchor, options) {
|
|
5653
|
+
if (!anchor || typeof anchor !== "object") return null;
|
|
5654
|
+
const note = normalizeReviewNote({
|
|
5655
|
+
id: makeRequestId(),
|
|
5656
|
+
text: "",
|
|
5657
|
+
createdAt: Date.now(),
|
|
5658
|
+
updatedAt: Date.now(),
|
|
5659
|
+
selectionStart: anchor.selectionStart,
|
|
5660
|
+
selectionEnd: anchor.selectionEnd,
|
|
5661
|
+
lineStart: anchor.lineStart,
|
|
5662
|
+
lineEnd: anchor.lineEnd,
|
|
5663
|
+
selectedText: anchor.selectedText,
|
|
5664
|
+
selectedDisplayText: typeof anchor.selectedDisplayText === "string" ? anchor.selectedDisplayText : (typeof anchor.selectedText === "string" ? anchor.selectedText : ""),
|
|
5665
|
+
});
|
|
5666
|
+
if (!note) return null;
|
|
5667
|
+
if (editorSelectionCommentBtn) {
|
|
5668
|
+
editorSelectionCommentBtn.hidden = true;
|
|
5669
|
+
}
|
|
5670
|
+
pendingReviewNoteFocusId = note.id;
|
|
5671
|
+
setReviewNotes(reviewNotes.concat([note]));
|
|
5672
|
+
if (!isReviewNotesOpen()) {
|
|
5673
|
+
openReviewNotes();
|
|
5674
|
+
}
|
|
5675
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5676
|
+
? window.requestAnimationFrame.bind(window)
|
|
5677
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5678
|
+
schedule(() => {
|
|
5679
|
+
updateEditorSelectionCommentUi();
|
|
5680
|
+
});
|
|
5681
|
+
if (!options || options.status !== false) {
|
|
5682
|
+
setStatus((options && options.statusMessage) || "Added local comment.", "success");
|
|
5683
|
+
}
|
|
5684
|
+
return note;
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
function addReviewNoteFromEditorSelection() {
|
|
5688
|
+
if (editorView !== "markdown") {
|
|
5689
|
+
setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
|
|
5690
|
+
return;
|
|
5691
|
+
}
|
|
5692
|
+
addReviewNoteFromAnchor(getEditorAnchorForReviewNote(), {
|
|
5693
|
+
statusMessage: "Added local comment.",
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
function addReviewNoteFromEditorLine() {
|
|
5698
|
+
if (editorView !== "markdown") {
|
|
5699
|
+
setStatus("Switch to Editor (Raw) before adding a line comment.", "warning");
|
|
5700
|
+
return;
|
|
5701
|
+
}
|
|
5702
|
+
addReviewNoteFromAnchor(getEditorLineAnchorForReviewNote(), {
|
|
5703
|
+
statusMessage: "Added local line comment.",
|
|
5704
|
+
});
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
function jumpToReviewNote(noteId) {
|
|
5708
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
5709
|
+
if (!note) return;
|
|
5710
|
+
const current = String(sourceTextEl.value || "");
|
|
5711
|
+
const range = resolveReviewNoteRange(note, current);
|
|
5712
|
+
if (!range) {
|
|
5713
|
+
setStatus("Could not find the anchored location for this comment.", "warning");
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
setEditorView("markdown");
|
|
5717
|
+
setActivePane("left");
|
|
5718
|
+
sourceTextEl.focus();
|
|
5719
|
+
sourceTextEl.setSelectionRange(range.start, range.end);
|
|
5720
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5721
|
+
? window.requestAnimationFrame.bind(window)
|
|
5722
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5723
|
+
schedule(() => {
|
|
5724
|
+
scrollEditorRangeIntoView(range);
|
|
5725
|
+
revealReviewNoteInPreview(note);
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
function deleteReviewNote(noteId) {
|
|
5730
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
5731
|
+
if (!note) return;
|
|
5732
|
+
const confirmed = window.confirm("Delete this local comment?");
|
|
5733
|
+
if (!confirmed) return;
|
|
5734
|
+
setReviewNotes(reviewNotes.filter((entry) => entry && entry.id !== noteId));
|
|
5735
|
+
setStatus("Deleted local comment.", "success");
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5738
|
+
function convertReviewNoteToAnnotation(noteId) {
|
|
5739
|
+
if (uiBusy) {
|
|
5740
|
+
setStatus("Wait until the current Studio action finishes before toggling inline annotation state.", "warning");
|
|
5741
|
+
return;
|
|
5742
|
+
}
|
|
5743
|
+
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
5744
|
+
if (!note) return;
|
|
5745
|
+
const current = String(sourceTextEl.value || "");
|
|
5746
|
+
const inlineState = getReviewNoteInlineState(note, current);
|
|
5747
|
+
if (!inlineState.annotationBody) {
|
|
5748
|
+
setStatus("Comment is empty. Add some text before toggling inline annotation state.", "warning");
|
|
5749
|
+
return;
|
|
5750
|
+
}
|
|
5751
|
+
if (!inlineState.range || !inlineState.canToggle) {
|
|
5752
|
+
setStatus("Could not find the anchored location for this comment.", "warning");
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
const next = inlineState.exists
|
|
5756
|
+
? current.slice(0, inlineState.range.end) + current.slice(inlineState.range.end + inlineState.markerText.length)
|
|
5757
|
+
: current.slice(0, inlineState.range.end) + inlineState.markerText + current.slice(inlineState.range.end);
|
|
5758
|
+
setEditorView("markdown");
|
|
5759
|
+
setEditorText(next, { preserveScroll: true, preserveSelection: true });
|
|
5760
|
+
pendingReviewNoteInlineFocusId = note.id;
|
|
5761
|
+
renderReviewNotesList();
|
|
5762
|
+
updateReviewNotesUi();
|
|
5763
|
+
setStatus(inlineState.exists ? "Removed inline annotation from local comment." : "Added inline annotation from local comment.", "success");
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
function toggleAllReviewNotesInlineAnnotations() {
|
|
5767
|
+
if (uiBusy) {
|
|
5768
|
+
setStatus("Wait until the current Studio action finishes before toggling inline annotations.", "warning");
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
const candidates = getDisplayReviewNotes().filter((note) => getReviewNoteInlineState(note, sourceTextEl.value || "").canToggle);
|
|
5772
|
+
if (candidates.length === 0) {
|
|
5773
|
+
setStatus("No non-empty comments are ready to toggle inline.", "warning");
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
let currentText = String(sourceTextEl.value || "");
|
|
5777
|
+
const shouldRemoveAll = candidates.every((note) => getReviewNoteInlineState(note, currentText).exists);
|
|
5778
|
+
const ordered = candidates
|
|
5779
|
+
.map((note) => ({ note, state: getReviewNoteInlineState(note, currentText) }))
|
|
5780
|
+
.filter((entry) => entry.state.range)
|
|
5781
|
+
.sort((left, right) => (right.state.range ? right.state.range.end : 0) - (left.state.range ? left.state.range.end : 0));
|
|
5782
|
+
|
|
5783
|
+
let changed = false;
|
|
5784
|
+
for (const entry of ordered) {
|
|
5785
|
+
const liveState = getReviewNoteInlineState(entry.note, currentText);
|
|
5786
|
+
if (!liveState.range || !liveState.canToggle) continue;
|
|
5787
|
+
if (shouldRemoveAll) {
|
|
5788
|
+
if (!liveState.exists) continue;
|
|
5789
|
+
currentText = currentText.slice(0, liveState.range.end) + currentText.slice(liveState.range.end + liveState.markerText.length);
|
|
5790
|
+
changed = true;
|
|
5791
|
+
} else {
|
|
5792
|
+
if (liveState.exists) continue;
|
|
5793
|
+
currentText = currentText.slice(0, liveState.range.end) + liveState.markerText + currentText.slice(liveState.range.end);
|
|
5794
|
+
changed = true;
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
if (!changed) {
|
|
5799
|
+
setStatus(shouldRemoveAll ? "No inline annotations were removed." : "No inline annotations were added.", "warning");
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
setEditorView("markdown");
|
|
5804
|
+
setEditorText(currentText, { preserveScroll: true, preserveSelection: true });
|
|
5805
|
+
renderReviewNotesList();
|
|
5806
|
+
updateReviewNotesUi();
|
|
5807
|
+
if (reviewNotesInlineAllBtn && typeof reviewNotesInlineAllBtn.focus === "function") {
|
|
5808
|
+
reviewNotesInlineAllBtn.focus();
|
|
5809
|
+
}
|
|
5810
|
+
setStatus(shouldRemoveAll ? "Removed inline annotations from all comments." : "Added inline annotations from all comments.", "success");
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
function updateScratchpadUi() {
|
|
5814
|
+
const normalized = String(scratchpadText || "");
|
|
5815
|
+
const hasContent = Boolean(normalized.trim());
|
|
5816
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
5817
|
+
if (scratchpadBtn) {
|
|
5818
|
+
scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
|
|
5819
|
+
scratchpadBtn.classList.toggle("has-content", hasContent);
|
|
5820
|
+
scratchpadBtn.title = hasContent
|
|
5821
|
+
? ("Open the local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.")
|
|
5822
|
+
: ("Open a local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.");
|
|
5823
|
+
}
|
|
5824
|
+
if (scratchpadMetaEl) {
|
|
5825
|
+
scratchpadMetaEl.textContent = hasContent
|
|
5826
|
+
? ("Saved locally for this document/draft · " + normalized.length + " chars")
|
|
5827
|
+
: "Empty · local to this document/draft";
|
|
5828
|
+
}
|
|
5829
|
+
if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
|
|
5830
|
+
if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
|
|
5831
|
+
if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
function setScratchpadText(nextText, options) {
|
|
5835
|
+
scratchpadText = String(nextText || "");
|
|
5836
|
+
if (scratchpadTextEl && scratchpadTextEl.value !== scratchpadText) {
|
|
5837
|
+
scratchpadTextEl.value = scratchpadText;
|
|
5838
|
+
}
|
|
5839
|
+
if (!options || options.persist !== false) {
|
|
5840
|
+
persistScratchpadText(scratchpadText);
|
|
5841
|
+
}
|
|
5842
|
+
updateScratchpadUi();
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
function closeScratchpad(options) {
|
|
5846
|
+
if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
|
|
5847
|
+
scratchpadOverlayEl.hidden = true;
|
|
5848
|
+
syncModalOpenState();
|
|
5849
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
5850
|
+
? options.focusTarget
|
|
5851
|
+
: (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
|
|
5852
|
+
scratchpadReturnFocusEl = null;
|
|
5853
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
5854
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5855
|
+
? window.requestAnimationFrame.bind(window)
|
|
5856
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5857
|
+
schedule(() => focusTarget.focus());
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
function openScratchpad() {
|
|
5862
|
+
if (!scratchpadOverlayEl) return;
|
|
5863
|
+
if (isReviewNotesOpen()) {
|
|
5864
|
+
closeReviewNotes({ focusTarget: null });
|
|
5865
|
+
}
|
|
5866
|
+
scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
5867
|
+
? document.activeElement
|
|
5868
|
+
: sourceTextEl;
|
|
5869
|
+
scratchpadOverlayEl.hidden = false;
|
|
5870
|
+
syncModalOpenState();
|
|
5871
|
+
if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
|
|
5872
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5873
|
+
? window.requestAnimationFrame.bind(window)
|
|
5874
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5875
|
+
schedule(() => {
|
|
5876
|
+
scratchpadTextEl.focus();
|
|
5877
|
+
if (typeof scratchpadTextEl.selectionStart === "number") {
|
|
5878
|
+
const end = scratchpadTextEl.value.length;
|
|
5879
|
+
scratchpadTextEl.setSelectionRange(end, end);
|
|
5880
|
+
}
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
function closeReviewNotes(options) {
|
|
5886
|
+
if (!reviewNotesOverlayEl || reviewNotesOverlayEl.hidden) return;
|
|
5887
|
+
reviewNotesOverlayEl.hidden = true;
|
|
5888
|
+
updateReviewNotesUi();
|
|
5889
|
+
if (editorView === "markdown") {
|
|
5890
|
+
scheduleEditorLineNumberRender();
|
|
5891
|
+
}
|
|
5892
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
5893
|
+
? options.focusTarget
|
|
5894
|
+
: (reviewNotesReturnFocusEl || reviewNotesBtn || sourceTextEl);
|
|
5895
|
+
reviewNotesReturnFocusEl = null;
|
|
5896
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
5897
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
5898
|
+
? window.requestAnimationFrame.bind(window)
|
|
5899
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
5900
|
+
schedule(() => focusTarget.focus());
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
function openReviewNotes() {
|
|
5905
|
+
if (!reviewNotesOverlayEl) return;
|
|
5906
|
+
if (isScratchpadOpen()) {
|
|
5907
|
+
closeScratchpad({ focusTarget: null });
|
|
5908
|
+
}
|
|
5909
|
+
reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
5910
|
+
? document.activeElement
|
|
5911
|
+
: sourceTextEl;
|
|
5912
|
+
reviewNotesOverlayEl.hidden = false;
|
|
5913
|
+
renderReviewNotesList();
|
|
5914
|
+
updateReviewNotesUi();
|
|
5915
|
+
if (editorView === "markdown") {
|
|
5916
|
+
scheduleEditorLineNumberRender();
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5920
|
+
function toggleReviewNotes() {
|
|
5921
|
+
if (isReviewNotesOpen()) {
|
|
5922
|
+
closeReviewNotes({ focusTarget: reviewNotesBtn || sourceTextEl });
|
|
5923
|
+
} else {
|
|
5924
|
+
openReviewNotes();
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
function insertScratchpadIntoEditor() {
|
|
5929
|
+
const content = String(scratchpadText || "");
|
|
5930
|
+
if (!content.trim()) {
|
|
5931
|
+
setStatus("Scratchpad is empty.", "warning");
|
|
5932
|
+
return;
|
|
5933
|
+
}
|
|
5934
|
+
|
|
5935
|
+
const current = sourceTextEl.value || "";
|
|
5936
|
+
const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
|
|
5937
|
+
const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
|
|
5938
|
+
const safeStart = Math.max(0, Math.min(start, current.length));
|
|
5939
|
+
const safeEnd = Math.max(safeStart, Math.min(end, current.length));
|
|
5940
|
+
const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
|
|
5941
|
+
setEditorText(next, { preserveScroll: false, preserveSelection: false });
|
|
5942
|
+
const caret = safeStart + content.length;
|
|
5943
|
+
sourceTextEl.setSelectionRange(caret, caret);
|
|
5944
|
+
setActivePane("left");
|
|
5945
|
+
closeScratchpad({ focusTarget: sourceTextEl });
|
|
5946
|
+
setStatus("Inserted scratchpad into editor.", "success");
|
|
5947
|
+
}
|
|
5948
|
+
|
|
5949
|
+
function updateEditorHighlightState() {
|
|
5950
|
+
const enabled = editorHighlightEnabled && editorView === "markdown";
|
|
5951
|
+
|
|
5952
|
+
sourceTextEl.classList.toggle("highlight-active", enabled);
|
|
5953
|
+
|
|
5954
|
+
if (sourceHighlightEl) {
|
|
5955
|
+
sourceHighlightEl.hidden = !enabled;
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
if (!enabled) {
|
|
5959
|
+
if (editorHighlightRenderRaf !== null) {
|
|
5960
|
+
if (typeof window.cancelAnimationFrame === "function") {
|
|
5961
|
+
window.cancelAnimationFrame(editorHighlightRenderRaf);
|
|
5962
|
+
} else {
|
|
5963
|
+
window.clearTimeout(editorHighlightRenderRaf);
|
|
5964
|
+
}
|
|
5965
|
+
editorHighlightRenderRaf = null;
|
|
5966
|
+
}
|
|
5967
|
+
|
|
5968
|
+
if (sourceHighlightEl) {
|
|
5969
|
+
sourceHighlightEl.innerHTML = "";
|
|
5970
|
+
sourceHighlightEl.scrollTop = 0;
|
|
5971
|
+
sourceHighlightEl.scrollLeft = 0;
|
|
5972
|
+
}
|
|
5973
|
+
return;
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
scheduleEditorHighlightRender();
|
|
5977
|
+
syncEditorHighlightScroll();
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
function syncHighlightSelectUi() {
|
|
5981
|
+
if (!highlightSelect) return;
|
|
5982
|
+
if (!editorHighlightEnabled) {
|
|
5983
|
+
highlightSelect.value = "off";
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
highlightSelect.value = (editorLanguage && SUPPORTED_LANGUAGES.indexOf(editorLanguage) !== -1)
|
|
5987
|
+
? editorLanguage
|
|
5988
|
+
: "markdown";
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5991
|
+
function setEditorHighlightEnabled(enabled) {
|
|
5992
|
+
editorHighlightEnabled = Boolean(enabled);
|
|
5993
|
+
persistEditorHighlightEnabled(editorHighlightEnabled);
|
|
5994
|
+
syncHighlightSelectUi();
|
|
5995
|
+
updateEditorHighlightState();
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5998
|
+
function readStoredEditorLanguage() {
|
|
5999
|
+
if (!window.localStorage) return null;
|
|
6000
|
+
try {
|
|
6001
|
+
const value = window.localStorage.getItem(EDITOR_LANGUAGE_STORAGE_KEY);
|
|
6002
|
+
if (value && SUPPORTED_LANGUAGES.indexOf(value) !== -1) return value;
|
|
6003
|
+
return null;
|
|
6004
|
+
} catch {
|
|
6005
|
+
return null;
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
|
|
6009
|
+
function persistEditorLanguage(lang) {
|
|
3812
6010
|
if (!window.localStorage) return;
|
|
3813
6011
|
try {
|
|
3814
6012
|
window.localStorage.setItem(EDITOR_LANGUAGE_STORAGE_KEY, lang || "markdown");
|
|
@@ -3932,8 +6130,8 @@
|
|
|
3932
6130
|
if (annotationModeSelect) {
|
|
3933
6131
|
annotationModeSelect.value = annotationsEnabled ? "on" : "off";
|
|
3934
6132
|
annotationModeSelect.title = annotationsEnabled
|
|
3935
|
-
? "
|
|
3936
|
-
: "
|
|
6133
|
+
? "Inline annotations On: keep and send [an: ...] markers."
|
|
6134
|
+
: "Inline annotations Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.";
|
|
3937
6135
|
}
|
|
3938
6136
|
|
|
3939
6137
|
syncRunAndCritiqueButtons();
|
|
@@ -4110,6 +6308,7 @@
|
|
|
4110
6308
|
|
|
4111
6309
|
let loadedInitialDocument = false;
|
|
4112
6310
|
if (
|
|
6311
|
+
!explicitDocumentIdentityFromUrl &&
|
|
4113
6312
|
!initialDocumentApplied &&
|
|
4114
6313
|
message.initialDocument &&
|
|
4115
6314
|
typeof message.initialDocument.text === "string"
|
|
@@ -4121,6 +6320,9 @@
|
|
|
4121
6320
|
source: message.initialDocument.source || "blank",
|
|
4122
6321
|
label: message.initialDocument.label || "blank",
|
|
4123
6322
|
path: message.initialDocument.path || null,
|
|
6323
|
+
draftId: typeof message.initialDocument.draftId === "string" && message.initialDocument.draftId.trim()
|
|
6324
|
+
? message.initialDocument.draftId.trim()
|
|
6325
|
+
: (initialSourceState.draftId || null),
|
|
4124
6326
|
});
|
|
4125
6327
|
if (message.initialDocument.path) {
|
|
4126
6328
|
markFileBackedBaseline(message.initialDocument.text);
|
|
@@ -4333,6 +6535,8 @@
|
|
|
4333
6535
|
source: "file",
|
|
4334
6536
|
label: message.label || message.path,
|
|
4335
6537
|
path: message.path,
|
|
6538
|
+
}, {
|
|
6539
|
+
carryCurrentMetadataToNewDocument: true,
|
|
4336
6540
|
});
|
|
4337
6541
|
markFileBackedBaseline(sourceTextEl.value);
|
|
4338
6542
|
}
|
|
@@ -4403,7 +6607,12 @@
|
|
|
4403
6607
|
: null;
|
|
4404
6608
|
|
|
4405
6609
|
setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
|
|
4406
|
-
setSourceState({
|
|
6610
|
+
setSourceState({
|
|
6611
|
+
source: nextSource,
|
|
6612
|
+
label: nextLabel,
|
|
6613
|
+
path: nextPath,
|
|
6614
|
+
draftId: typeof nextDoc.draftId === "string" && nextDoc.draftId.trim() ? nextDoc.draftId.trim() : null,
|
|
6615
|
+
});
|
|
4407
6616
|
if (nextPath) {
|
|
4408
6617
|
markFileBackedBaseline(nextDoc.text);
|
|
4409
6618
|
}
|
|
@@ -4855,6 +7064,8 @@
|
|
|
4855
7064
|
window.addEventListener("keydown", handlePaneShortcut);
|
|
4856
7065
|
window.addEventListener("beforeunload", () => {
|
|
4857
7066
|
stopFooterSpinner();
|
|
7067
|
+
flushScratchpadPersistence();
|
|
7068
|
+
flushReviewNotesPersistence();
|
|
4858
7069
|
});
|
|
4859
7070
|
|
|
4860
7071
|
editorViewSelect.addEventListener("change", () => {
|
|
@@ -5004,8 +7215,41 @@
|
|
|
5004
7215
|
});
|
|
5005
7216
|
|
|
5006
7217
|
sourceTextEl.addEventListener("input", () => {
|
|
7218
|
+
if (activePreviewCommentSelection) {
|
|
7219
|
+
clearPreviewCommentSelection();
|
|
7220
|
+
}
|
|
5007
7221
|
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
5008
7222
|
scheduleEditorMetaUpdate();
|
|
7223
|
+
updateEditorSelectionCommentUi();
|
|
7224
|
+
if (isReviewNotesOpen() && reviewNotes.length > 0) {
|
|
7225
|
+
renderReviewNotesList();
|
|
7226
|
+
updateReviewNotesUi();
|
|
7227
|
+
}
|
|
7228
|
+
});
|
|
7229
|
+
|
|
7230
|
+
sourceTextEl.addEventListener("select", () => {
|
|
7231
|
+
updateEditorSelectionCommentUi();
|
|
7232
|
+
});
|
|
7233
|
+
|
|
7234
|
+
sourceTextEl.addEventListener("keyup", () => {
|
|
7235
|
+
updateEditorSelectionCommentUi();
|
|
7236
|
+
});
|
|
7237
|
+
|
|
7238
|
+
sourceTextEl.addEventListener("mouseup", () => {
|
|
7239
|
+
updateEditorSelectionCommentUi();
|
|
7240
|
+
});
|
|
7241
|
+
|
|
7242
|
+
sourceTextEl.addEventListener("focus", () => {
|
|
7243
|
+
updateEditorSelectionCommentUi();
|
|
7244
|
+
});
|
|
7245
|
+
|
|
7246
|
+
sourceTextEl.addEventListener("blur", () => {
|
|
7247
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
7248
|
+
? window.requestAnimationFrame.bind(window)
|
|
7249
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
7250
|
+
schedule(() => {
|
|
7251
|
+
updateEditorSelectionCommentUi();
|
|
7252
|
+
});
|
|
5009
7253
|
});
|
|
5010
7254
|
|
|
5011
7255
|
sourceTextEl.addEventListener("scroll", () => {
|
|
@@ -5026,9 +7270,7 @@
|
|
|
5026
7270
|
window.addEventListener("resize", () => {
|
|
5027
7271
|
if (editorView !== "markdown") return;
|
|
5028
7272
|
syncEditorHighlightScroll();
|
|
5029
|
-
|
|
5030
|
-
scheduleEditorLineNumberRender();
|
|
5031
|
-
}
|
|
7273
|
+
scheduleEditorLineNumberRender();
|
|
5032
7274
|
});
|
|
5033
7275
|
|
|
5034
7276
|
insertHeaderBtn.addEventListener("click", () => {
|
|
@@ -5353,6 +7595,92 @@
|
|
|
5353
7595
|
}
|
|
5354
7596
|
});
|
|
5355
7597
|
|
|
7598
|
+
if (reviewNotesBtn) {
|
|
7599
|
+
reviewNotesBtn.addEventListener("click", () => {
|
|
7600
|
+
toggleReviewNotes();
|
|
7601
|
+
});
|
|
7602
|
+
}
|
|
7603
|
+
|
|
7604
|
+
if (reviewNotesCloseBtn) {
|
|
7605
|
+
reviewNotesCloseBtn.addEventListener("click", () => {
|
|
7606
|
+
closeReviewNotes();
|
|
7607
|
+
});
|
|
7608
|
+
}
|
|
7609
|
+
|
|
7610
|
+
if (reviewNotesDoneBtn) {
|
|
7611
|
+
reviewNotesDoneBtn.addEventListener("click", () => {
|
|
7612
|
+
closeReviewNotes();
|
|
7613
|
+
});
|
|
7614
|
+
}
|
|
7615
|
+
|
|
7616
|
+
if (reviewNotesAddBtn) {
|
|
7617
|
+
reviewNotesAddBtn.addEventListener("click", () => {
|
|
7618
|
+
addReviewNoteFromEditorLine();
|
|
7619
|
+
});
|
|
7620
|
+
}
|
|
7621
|
+
|
|
7622
|
+
if (editorSelectionCommentBtn) {
|
|
7623
|
+
editorSelectionCommentBtn.addEventListener("mousedown", (event) => {
|
|
7624
|
+
event.preventDefault();
|
|
7625
|
+
});
|
|
7626
|
+
editorSelectionCommentBtn.addEventListener("click", () => {
|
|
7627
|
+
addReviewNoteFromEditorSelection();
|
|
7628
|
+
});
|
|
7629
|
+
}
|
|
7630
|
+
|
|
7631
|
+
if (reviewNotesInlineAllBtn) {
|
|
7632
|
+
reviewNotesInlineAllBtn.addEventListener("click", () => {
|
|
7633
|
+
toggleAllReviewNotesInlineAnnotations();
|
|
7634
|
+
});
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
if (reviewNoteGutterContentEl) {
|
|
7638
|
+
reviewNoteGutterContentEl.addEventListener("click", (event) => {
|
|
7639
|
+
const target = event.target;
|
|
7640
|
+
const markerBtn = target instanceof Element ? target.closest(".editor-review-note-marker") : null;
|
|
7641
|
+
if (!markerBtn) return;
|
|
7642
|
+
const noteId = markerBtn.getAttribute("data-review-note-id") || "";
|
|
7643
|
+
if (!noteId) return;
|
|
7644
|
+
focusReviewNoteInPanel(noteId);
|
|
7645
|
+
});
|
|
7646
|
+
}
|
|
7647
|
+
|
|
7648
|
+
function handlePreviewCommentActionMouseDown(event) {
|
|
7649
|
+
const target = event.target;
|
|
7650
|
+
const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
|
|
7651
|
+
if (!actionBtn) return;
|
|
7652
|
+
event.preventDefault();
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
function handlePreviewCommentActionClick(event) {
|
|
7656
|
+
const target = event.target;
|
|
7657
|
+
const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
|
|
7658
|
+
if (!actionBtn) return;
|
|
7659
|
+
const blockEl = actionBtn.closest(".preview-comment-block");
|
|
7660
|
+
if (!blockEl) return;
|
|
7661
|
+
event.preventDefault();
|
|
7662
|
+
event.stopPropagation();
|
|
7663
|
+
const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
|
|
7664
|
+
if (mode !== "selection") return;
|
|
7665
|
+
addReviewNoteFromPreviewSelection(blockEl);
|
|
7666
|
+
}
|
|
7667
|
+
|
|
7668
|
+
if (leftPaneEl) {
|
|
7669
|
+
leftPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
|
|
7670
|
+
leftPaneEl.addEventListener("click", handlePreviewCommentActionClick);
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7673
|
+
if (rightPaneEl) {
|
|
7674
|
+
rightPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
|
|
7675
|
+
rightPaneEl.addEventListener("click", handlePreviewCommentActionClick);
|
|
7676
|
+
}
|
|
7677
|
+
|
|
7678
|
+
if (typeof document.addEventListener === "function") {
|
|
7679
|
+
document.addEventListener("selectionchange", () => {
|
|
7680
|
+
updateActivePreviewCommentSelectionFromDom();
|
|
7681
|
+
});
|
|
7682
|
+
}
|
|
7683
|
+
|
|
5356
7684
|
if (scratchpadBtn) {
|
|
5357
7685
|
scratchpadBtn.addEventListener("click", () => {
|
|
5358
7686
|
openScratchpad();
|
|
@@ -5487,6 +7815,11 @@
|
|
|
5487
7815
|
syncActionButtons();
|
|
5488
7816
|
renderSourcePreview();
|
|
5489
7817
|
}
|
|
7818
|
+
if (sourceBadgeEl) {
|
|
7819
|
+
sourceBadgeEl.addEventListener("click", () => {
|
|
7820
|
+
resetEditorOrigin();
|
|
7821
|
+
});
|
|
7822
|
+
}
|
|
5490
7823
|
if (resourceDirBtn) {
|
|
5491
7824
|
resourceDirBtn.addEventListener("click", () => {
|
|
5492
7825
|
showResourceDirState("input");
|
|
@@ -5558,7 +7891,7 @@
|
|
|
5558
7891
|
|
|
5559
7892
|
if (sourceEditorWrapEl && typeof ResizeObserver === "function") {
|
|
5560
7893
|
const editorResizeObserver = new ResizeObserver(() => {
|
|
5561
|
-
if (editorView !== "markdown"
|
|
7894
|
+
if (editorView !== "markdown") return;
|
|
5562
7895
|
scheduleEditorLineNumberRender();
|
|
5563
7896
|
});
|
|
5564
7897
|
editorResizeObserver.observe(sourceEditorWrapEl);
|
|
@@ -5568,7 +7901,6 @@
|
|
|
5568
7901
|
refreshResponseUi();
|
|
5569
7902
|
updateAnnotatedReplyHeaderButton();
|
|
5570
7903
|
setActivePane("left");
|
|
5571
|
-
setScratchpadText(readStoredScratchpadText() || "", { persist: false });
|
|
5572
7904
|
|
|
5573
7905
|
const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
|
|
5574
7906
|
const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value !== "off");
|