pi-studio 0.5.45 → 0.5.47

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.
@@ -53,6 +53,7 @@
53
53
  const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
54
54
  const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
55
55
  const sourcePreviewEl = document.getElementById("sourcePreview");
56
+ const editorSelectionCommentBtn = document.getElementById("editorSelectionCommentBtn");
56
57
  const leftPaneEl = document.getElementById("leftPane");
57
58
  const rightPaneEl = document.getElementById("rightPane");
58
59
  const sourceBadgeEl = document.getElementById("sourceBadge");
@@ -304,6 +305,11 @@
304
305
  let reviewNotesLoadNonce = 0;
305
306
  let pendingReviewNoteFocusId = null;
306
307
  let pendingReviewNoteInlineFocusId = null;
308
+ let activePreviewCommentSelection = null;
309
+ let suppressEditorSelectionComment = false;
310
+ let suppressedEditorSelectionStart = null;
311
+ let suppressedEditorSelectionEnd = null;
312
+ const previewJumpHighlightState = new WeakMap();
307
313
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
308
314
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
309
315
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
@@ -2528,6 +2534,7 @@
2528
2534
  if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
2529
2535
  }
2530
2536
 
2537
+ clearPreviewJumpHighlight(targetEl);
2531
2538
  finishPreviewRender(targetEl);
2532
2539
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2533
2540
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
@@ -2541,6 +2548,15 @@
2541
2548
  await renderMermaidInElement(targetEl);
2542
2549
  await renderMathFallbackInElement(targetEl);
2543
2550
 
2551
+ const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
2552
+ && (
2553
+ (pane === "source" && editorView === "preview")
2554
+ || (pane === "response" && rightView === "editor-preview")
2555
+ );
2556
+ if (shouldDecoratePreviewComments) {
2557
+ decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
2558
+ }
2559
+
2544
2560
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
2545
2561
  if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
2546
2562
  var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
@@ -2562,6 +2578,7 @@
2562
2578
  }
2563
2579
 
2564
2580
  const detail = error && error.message ? error.message : String(error || "unknown error");
2581
+ clearPreviewJumpHighlight(targetEl);
2565
2582
  finishPreviewRender(targetEl);
2566
2583
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2567
2584
  if (pane === "response") {
@@ -2905,6 +2922,9 @@
2905
2922
  const value = String(nextText || "");
2906
2923
  const preserveScroll = Boolean(options && options.preserveScroll);
2907
2924
  const preserveSelection = Boolean(options && options.preserveSelection);
2925
+ if (activePreviewCommentSelection) {
2926
+ clearPreviewCommentSelection();
2927
+ }
2908
2928
  const previousScrollTop = sourceTextEl.scrollTop;
2909
2929
  const previousScrollLeft = sourceTextEl.scrollLeft;
2910
2930
  const previousSelectionStart = sourceTextEl.selectionStart;
@@ -2943,6 +2963,7 @@
2943
2963
  if (!options || options.updateMeta !== false) {
2944
2964
  scheduleEditorMetaUpdate();
2945
2965
  }
2966
+ updateEditorSelectionCommentUi();
2946
2967
  }
2947
2968
 
2948
2969
  function setEditorView(nextView) {
@@ -2961,6 +2982,7 @@
2961
2982
  }
2962
2983
 
2963
2984
  if (!showPreview) {
2985
+ clearPreviewJumpHighlight(sourcePreviewEl);
2964
2986
  finishPreviewRender(sourcePreviewEl);
2965
2987
  }
2966
2988
 
@@ -2975,6 +2997,7 @@
2975
2997
  scheduleEditorLineNumberRender();
2976
2998
  }
2977
2999
  updateReviewNotesUi();
3000
+ updateEditorSelectionCommentUi();
2978
3001
  }
2979
3002
 
2980
3003
  function setRightView(nextView) {
@@ -2989,6 +3012,9 @@
2989
3012
  window.clearTimeout(responseEditorPreviewTimer);
2990
3013
  responseEditorPreviewTimer = null;
2991
3014
  }
3015
+ if (rightView !== "editor-preview") {
3016
+ clearPreviewJumpHighlight(critiqueViewEl);
3017
+ }
2992
3018
 
2993
3019
  refreshResponseUi();
2994
3020
  syncActionButtons();
@@ -4082,6 +4108,7 @@
4082
4108
  lineStart,
4083
4109
  lineEnd,
4084
4110
  selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
4111
+ selectedDisplayText: typeof note.selectedDisplayText === "string" ? note.selectedDisplayText : "",
4085
4112
  };
4086
4113
  }
4087
4114
 
@@ -4171,6 +4198,7 @@
4171
4198
  }
4172
4199
  updateReviewNotesUi();
4173
4200
  renderReviewNotesList();
4201
+ refreshRenderedEditorPreviewComments();
4174
4202
  if (editorView === "markdown") {
4175
4203
  scheduleEditorLineNumberRender();
4176
4204
  }
@@ -4192,7 +4220,7 @@
4192
4220
  }
4193
4221
 
4194
4222
  function summarizeReviewNoteQuote(note) {
4195
- const normalized = String(note && note.selectedText ? note.selectedText : "")
4223
+ const normalized = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
4196
4224
  .replace(/\s+/g, " ")
4197
4225
  .trim();
4198
4226
  if (!normalized) return "Anchor: current line / empty selection";
@@ -4252,6 +4280,7 @@
4252
4280
  lineStart: getLineNumberAtOffset(current, safeStart),
4253
4281
  lineEnd: getLineNumberAtOffset(current, Math.max(safeStart, safeEnd - 1)),
4254
4282
  selectedText: current.slice(safeStart, safeEnd),
4283
+ selectedDisplayText: current.slice(safeStart, safeEnd),
4255
4284
  };
4256
4285
  }
4257
4286
  const lineRange = getLineRangeAtOffset(current, safeStart);
@@ -4261,6 +4290,23 @@
4261
4290
  lineStart: lineRange.lineNumber,
4262
4291
  lineEnd: lineRange.lineNumber,
4263
4292
  selectedText: current.slice(lineRange.start, lineRange.end),
4293
+ selectedDisplayText: current.slice(lineRange.start, lineRange.end),
4294
+ };
4295
+ }
4296
+
4297
+ function getEditorLineAnchorForReviewNote() {
4298
+ const current = String(sourceTextEl.value || "");
4299
+ const caret = typeof sourceTextEl.selectionStart === "number"
4300
+ ? sourceTextEl.selectionStart
4301
+ : 0;
4302
+ const lineRange = getLineRangeAtOffset(current, Math.max(0, Math.min(caret, current.length)));
4303
+ return {
4304
+ selectionStart: lineRange.start,
4305
+ selectionEnd: lineRange.end,
4306
+ lineStart: lineRange.lineNumber,
4307
+ lineEnd: lineRange.lineNumber,
4308
+ selectedText: current.slice(lineRange.start, lineRange.end),
4309
+ selectedDisplayText: current.slice(lineRange.start, lineRange.end),
4264
4310
  };
4265
4311
  }
4266
4312
 
@@ -4314,6 +4360,972 @@
4314
4360
  return lineMap;
4315
4361
  }
4316
4362
 
4363
+ function supportsPreviewCommentsForCurrentEditor() {
4364
+ return editorLanguage === "markdown";
4365
+ }
4366
+
4367
+ function getPreviewCommentBlockKindLabel(kind) {
4368
+ if (kind === "heading") return "heading";
4369
+ if (kind === "blockquote") return "quote block";
4370
+ if (kind === "list") return "list";
4371
+ if (kind === "code") return "code block";
4372
+ if (kind === "table") return "table";
4373
+ return "paragraph";
4374
+ }
4375
+
4376
+ function supportsPreviewSelectionCommentsForBlockKind(kind) {
4377
+ return kind === "paragraph" || kind === "heading" || kind === "blockquote" || kind === "list";
4378
+ }
4379
+
4380
+ function normalizeVisiblePreviewText(text) {
4381
+ return String(text || "").replace(/\s+/g, " ").trim();
4382
+ }
4383
+
4384
+ function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
4385
+ const safeStart = Math.max(0, Math.min(start, lineText.length));
4386
+ const safeEnd = Math.max(safeStart, Math.min(end, lineText.length));
4387
+ for (let i = safeStart; i < safeEnd; i += 1) {
4388
+ chars.push(lineText[i]);
4389
+ rawOffsets.push(lineBaseOffset + i);
4390
+ }
4391
+ }
4392
+
4393
+ function buildPreviewSelectionSourceBody(blockText, kind) {
4394
+ const source = String(blockText || "");
4395
+ const lines = source.split("\n");
4396
+ const lineOffsets = [];
4397
+ let runningOffset = 0;
4398
+ for (const line of lines) {
4399
+ lineOffsets.push(runningOffset);
4400
+ runningOffset += line.length + 1;
4401
+ }
4402
+
4403
+ const chars = [];
4404
+ const rawOffsets = [];
4405
+
4406
+ function appendLineWithStart(lineIndex, start, end) {
4407
+ const line = lineIndex >= 0 && lineIndex < lines.length ? lines[lineIndex] : "";
4408
+ appendMappedPreviewSlice(chars, rawOffsets, line, lineOffsets[lineIndex] || 0, start, end);
4409
+ if (lineIndex < lines.length - 1) {
4410
+ chars.push("\n");
4411
+ rawOffsets.push((lineOffsets[lineIndex] || 0) + line.length);
4412
+ }
4413
+ }
4414
+
4415
+ if (kind === "heading") {
4416
+ const firstLine = lines[0] || "";
4417
+ const atxMatch = firstLine.match(/^ {0,3}#{1,6}(?:[ \t]+|$)/);
4418
+ if (atxMatch) {
4419
+ const start = atxMatch[0].length;
4420
+ let end = firstLine.length;
4421
+ const closingMatch = firstLine.slice(start).match(/[ \t]+#+[ \t]*$/);
4422
+ if (closingMatch) {
4423
+ end -= closingMatch[0].length;
4424
+ }
4425
+ appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, start, end);
4426
+ return { text: chars.join(""), rawOffsets };
4427
+ }
4428
+ if (lines.length >= 2 && /^ {0,3}(?:={3,}|-{3,})\s*$/.test(lines[1] || "")) {
4429
+ appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, 0, firstLine.length);
4430
+ return { text: chars.join(""), rawOffsets };
4431
+ }
4432
+ }
4433
+
4434
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4435
+ const line = lines[lineIndex] || "";
4436
+ if (kind === "blockquote") {
4437
+ const prefixMatch = line.match(/^ {0,3}> ?/);
4438
+ appendLineWithStart(lineIndex, prefixMatch ? prefixMatch[0].length : 0, line.length);
4439
+ continue;
4440
+ }
4441
+ if (kind === "list") {
4442
+ if (!line.trim()) {
4443
+ appendLineWithStart(lineIndex, 0, 0);
4444
+ continue;
4445
+ }
4446
+ const itemMatch = line.match(/^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/);
4447
+ if (itemMatch) {
4448
+ appendLineWithStart(lineIndex, itemMatch[0].length, line.length);
4449
+ continue;
4450
+ }
4451
+ const continuationMatch = line.match(/^(?: {1,4}|\t)/);
4452
+ appendLineWithStart(lineIndex, continuationMatch ? continuationMatch[0].length : 0, line.length);
4453
+ continue;
4454
+ }
4455
+ appendLineWithStart(lineIndex, 0, line.length);
4456
+ }
4457
+
4458
+ return { text: chars.join(""), rawOffsets };
4459
+ }
4460
+
4461
+ function buildPreviewInlineDisplayMap(text, rawOffsets) {
4462
+ const source = String(text || "");
4463
+ const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
4464
+ const displayChars = [];
4465
+ const charStarts = [];
4466
+ const charEnds = [];
4467
+
4468
+ function appendChar(character, rawStart, rawEnd) {
4469
+ displayChars.push(character);
4470
+ charStarts.push(rawStart);
4471
+ charEnds.push(rawEnd);
4472
+ }
4473
+
4474
+ function appendNestedRange(startIndex, endIndex) {
4475
+ const nested = buildPreviewInlineDisplayMap(
4476
+ source.slice(startIndex, endIndex),
4477
+ rawMap.slice(startIndex, endIndex),
4478
+ );
4479
+ for (let i = 0; i < nested.text.length; i += 1) {
4480
+ appendChar(nested.text[i], nested.charStarts[i], nested.charEnds[i]);
4481
+ }
4482
+ }
4483
+
4484
+ let index = 0;
4485
+ while (index < source.length) {
4486
+ const remaining = source.slice(index);
4487
+ const linkMatch = remaining.match(/^!?\[([^\]]*)\]\(([^)]*)\)/);
4488
+ if (linkMatch) {
4489
+ const labelStart = index + (remaining[0] === "!" ? 2 : 1);
4490
+ const labelEnd = labelStart + String(linkMatch[1] || "").length;
4491
+ appendNestedRange(labelStart, labelEnd);
4492
+ index += linkMatch[0].length;
4493
+ continue;
4494
+ }
4495
+
4496
+ if (source[index] === "`") {
4497
+ let tickCount = 1;
4498
+ while (source[index + tickCount] === "`") tickCount += 1;
4499
+ const fence = "`".repeat(tickCount);
4500
+ const closeIndex = source.indexOf(fence, index + tickCount);
4501
+ if (closeIndex >= 0) {
4502
+ for (let i = index + tickCount; i < closeIndex; i += 1) {
4503
+ appendChar(source[i], rawMap[i], rawMap[i] + 1);
4504
+ }
4505
+ index = closeIndex + tickCount;
4506
+ continue;
4507
+ }
4508
+ }
4509
+
4510
+ if (source[index] === "\\" && index + 1 < source.length) {
4511
+ appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
4512
+ index += 2;
4513
+ continue;
4514
+ }
4515
+
4516
+ const htmlTagMatch = remaining.match(/^<\/?[A-Za-z][^>]*>/);
4517
+ if (htmlTagMatch) {
4518
+ index += htmlTagMatch[0].length;
4519
+ continue;
4520
+ }
4521
+
4522
+ const emphasisMatch = remaining.match(/^(?:\*\*\*|\*\*|\*|___|__|_|~~)/);
4523
+ if (emphasisMatch) {
4524
+ index += emphasisMatch[0].length;
4525
+ continue;
4526
+ }
4527
+
4528
+ appendChar(source[index], rawMap[index], rawMap[index] + 1);
4529
+ index += 1;
4530
+ }
4531
+
4532
+ return {
4533
+ text: displayChars.join(""),
4534
+ charStarts,
4535
+ charEnds,
4536
+ };
4537
+ }
4538
+
4539
+ function buildNormalizedPreviewDisplayMap(displayText, charStarts, charEnds) {
4540
+ const source = String(displayText || "");
4541
+ const outChars = [];
4542
+ const outStarts = [];
4543
+ const outEnds = [];
4544
+ let pendingWhitespaceStart = null;
4545
+ let pendingWhitespaceEnd = null;
4546
+
4547
+ for (let i = 0; i < source.length; i += 1) {
4548
+ const character = source[i];
4549
+ if (/\s/.test(character)) {
4550
+ if (outChars.length === 0) continue;
4551
+ if (pendingWhitespaceStart == null) {
4552
+ pendingWhitespaceStart = charStarts[i];
4553
+ }
4554
+ pendingWhitespaceEnd = charEnds[i];
4555
+ continue;
4556
+ }
4557
+
4558
+ if (pendingWhitespaceStart != null && pendingWhitespaceEnd != null) {
4559
+ outChars.push(" ");
4560
+ outStarts.push(pendingWhitespaceStart);
4561
+ outEnds.push(pendingWhitespaceEnd);
4562
+ pendingWhitespaceStart = null;
4563
+ pendingWhitespaceEnd = null;
4564
+ }
4565
+
4566
+ outChars.push(character);
4567
+ outStarts.push(charStarts[i]);
4568
+ outEnds.push(charEnds[i]);
4569
+ }
4570
+
4571
+ return {
4572
+ text: outChars.join(""),
4573
+ charStarts: outStarts,
4574
+ charEnds: outEnds,
4575
+ };
4576
+ }
4577
+
4578
+ function buildNormalizedDomTextMap(rootEl) {
4579
+ if (!rootEl || typeof document.createTreeWalker !== "function") {
4580
+ return { text: "", charStarts: [], charEnds: [] };
4581
+ }
4582
+ const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT);
4583
+ const chars = [];
4584
+ const starts = [];
4585
+ const ends = [];
4586
+ let node = walker.nextNode();
4587
+ while (node) {
4588
+ const textNode = node;
4589
+ const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
4590
+ for (let i = 0; i < value.length; i += 1) {
4591
+ chars.push(value[i]);
4592
+ starts.push({ node: textNode, offset: i });
4593
+ ends.push({ node: textNode, offset: i + 1 });
4594
+ }
4595
+ node = walker.nextNode();
4596
+ }
4597
+ return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
4598
+ }
4599
+
4600
+ function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
4601
+ const source = String(haystack || "");
4602
+ const query = String(needle || "");
4603
+ if (!source || !query) return -1;
4604
+ let bestIndex = -1;
4605
+ let bestScore = Number.POSITIVE_INFINITY;
4606
+ const desiredIndex = Number.isFinite(preferredIndex) ? Math.max(0, preferredIndex) : 0;
4607
+ for (let matchIndex = source.indexOf(query); matchIndex >= 0; matchIndex = source.indexOf(query, matchIndex + 1)) {
4608
+ const score = Math.abs(matchIndex - desiredIndex);
4609
+ if (score < bestScore) {
4610
+ bestScore = score;
4611
+ bestIndex = matchIndex;
4612
+ }
4613
+ }
4614
+ return bestIndex;
4615
+ }
4616
+
4617
+ function buildPreviewSelectionDisplayMap(blockText, kind) {
4618
+ const body = buildPreviewSelectionSourceBody(blockText, kind);
4619
+ const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
4620
+ return buildNormalizedPreviewDisplayMap(inlineMap.text, inlineMap.charStarts, inlineMap.charEnds);
4621
+ }
4622
+
4623
+ function getPreviewCommentBlockKey(blockEl) {
4624
+ if (!blockEl || !blockEl.dataset) return "";
4625
+ return [
4626
+ String(blockEl.dataset.reviewNoteStart || ""),
4627
+ String(blockEl.dataset.reviewNoteEnd || ""),
4628
+ String(blockEl.dataset.previewCommentKind || ""),
4629
+ ].join(":");
4630
+ }
4631
+
4632
+ function getPreviewCommentSelectionKey(selection) {
4633
+ if (!selection) return "";
4634
+ return [
4635
+ String(selection.blockKey || ""),
4636
+ String(selection.selectionStart || 0),
4637
+ String(selection.selectionEnd || 0),
4638
+ String(selection.selectedDisplayText || ""),
4639
+ ].join(":");
4640
+ }
4641
+
4642
+ function setActivePreviewCommentSelection(nextSelection) {
4643
+ const currentKey = getPreviewCommentSelectionKey(activePreviewCommentSelection);
4644
+ const nextKey = getPreviewCommentSelectionKey(nextSelection);
4645
+ if (currentKey === nextKey) return;
4646
+ activePreviewCommentSelection = nextSelection || null;
4647
+ refreshRenderedEditorPreviewComments();
4648
+ }
4649
+
4650
+ function clearPreviewCommentSelection() {
4651
+ setActivePreviewCommentSelection(null);
4652
+ }
4653
+
4654
+ function findPreviewCommentBlockFromNode(node) {
4655
+ if (!node) return null;
4656
+ const element = node instanceof Element ? node : node.parentElement;
4657
+ return element && typeof element.closest === "function"
4658
+ ? element.closest(".preview-comment-block")
4659
+ : null;
4660
+ }
4661
+
4662
+ function unwrapPreviewJumpHighlightElement(element) {
4663
+ if (!element || !element.parentNode) return;
4664
+ const parent = element.parentNode;
4665
+ while (element.firstChild) {
4666
+ parent.insertBefore(element.firstChild, element);
4667
+ }
4668
+ parent.removeChild(element);
4669
+ if (typeof parent.normalize === "function") {
4670
+ parent.normalize();
4671
+ }
4672
+ }
4673
+
4674
+ function clearPreviewJumpHighlight(targetEl) {
4675
+ if (!targetEl) return;
4676
+ const state = previewJumpHighlightState.get(targetEl);
4677
+ if (!state) return;
4678
+ if (state.timer != null) {
4679
+ window.clearTimeout(state.timer);
4680
+ }
4681
+ if (state.inlineHighlightEl) {
4682
+ unwrapPreviewJumpHighlightElement(state.inlineHighlightEl);
4683
+ }
4684
+ if (state.contentEl && state.contentEl.classList) {
4685
+ state.contentEl.classList.remove("preview-jump-highlight");
4686
+ }
4687
+ previewJumpHighlightState.delete(targetEl);
4688
+ }
4689
+
4690
+ function setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl) {
4691
+ if (!targetEl || !contentEl) return;
4692
+ clearPreviewJumpHighlight(targetEl);
4693
+ if (contentEl.classList) {
4694
+ contentEl.classList.add("preview-jump-highlight");
4695
+ }
4696
+ const timer = window.setTimeout(() => {
4697
+ clearPreviewJumpHighlight(targetEl);
4698
+ }, 1800);
4699
+ previewJumpHighlightState.set(targetEl, {
4700
+ contentEl,
4701
+ inlineHighlightEl: inlineHighlightEl || null,
4702
+ timer,
4703
+ });
4704
+ }
4705
+
4706
+ function rangesOverlap(startA, endA, startB, endB) {
4707
+ const safeStartA = Math.max(0, Number(startA) || 0);
4708
+ const safeStartB = Math.max(0, Number(startB) || 0);
4709
+ const safeEndA = Math.max(safeStartA + 1, Number(endA) || safeStartA);
4710
+ const safeEndB = Math.max(safeStartB + 1, Number(endB) || safeStartB);
4711
+ return safeStartA < safeEndB && safeStartB < safeEndA;
4712
+ }
4713
+
4714
+ function scanMarkdownPreviewCommentBlocks(markdown) {
4715
+ const source = String(markdown || "").replace(/\r\n/g, "\n");
4716
+ const lines = source.split("\n");
4717
+ const lineOffsets = [];
4718
+ let runningOffset = 0;
4719
+ for (const line of lines) {
4720
+ lineOffsets.push(runningOffset);
4721
+ runningOffset += line.length + 1;
4722
+ }
4723
+
4724
+ function getLine(index) {
4725
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
4726
+ }
4727
+
4728
+ function isBlankLine(index) {
4729
+ return /^\s*$/.test(getLine(index));
4730
+ }
4731
+
4732
+ function lineStartsFence(index) {
4733
+ return getLine(index).match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
4734
+ }
4735
+
4736
+ function isAtxHeadingLine(index) {
4737
+ return /^ {0,3}#{1,6}(?:[ \t]+|$)/.test(getLine(index));
4738
+ }
4739
+
4740
+ function isSetextUnderlineLine(index) {
4741
+ return /^ {0,3}(?:={3,}|-{3,})\s*$/.test(getLine(index));
4742
+ }
4743
+
4744
+ function isThematicBreakLine(index) {
4745
+ return /^ {0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/.test(getLine(index));
4746
+ }
4747
+
4748
+ function isBlockquoteLine(index) {
4749
+ return /^ {0,3}> ?/.test(getLine(index));
4750
+ }
4751
+
4752
+ function isListLine(index) {
4753
+ return /^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/.test(getLine(index));
4754
+ }
4755
+
4756
+ function isContinuationIndentedLine(index) {
4757
+ return /^(?: {2,}|\t+)/.test(getLine(index));
4758
+ }
4759
+
4760
+ function isPotentialTableRow(index) {
4761
+ const line = getLine(index);
4762
+ return /\|/.test(line) && !/^\s*</.test(line);
4763
+ }
4764
+
4765
+ function isTableDividerLine(index) {
4766
+ return /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/.test(getLine(index));
4767
+ }
4768
+
4769
+ function isHtmlCommentStart(index) {
4770
+ return /^\s*<!--/.test(getLine(index));
4771
+ }
4772
+
4773
+ function makeBlock(kind, startLineIndex, endLineIndex) {
4774
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
4775
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
4776
+ const start = lineOffsets[safeStartLine] || 0;
4777
+ const end = (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
4778
+ return {
4779
+ kind,
4780
+ start,
4781
+ end,
4782
+ lineStart: safeStartLine + 1,
4783
+ lineEnd: safeEndLine + 1,
4784
+ };
4785
+ }
4786
+
4787
+ const blocks = [];
4788
+ let index = 0;
4789
+
4790
+ if (/^\s*---\s*$/.test(getLine(0))) {
4791
+ for (let i = 1; i < Math.min(lines.length, 80); i += 1) {
4792
+ if (/^\s*(?:---|\.\.\.)\s*$/.test(getLine(i))) {
4793
+ index = i + 1;
4794
+ break;
4795
+ }
4796
+ }
4797
+ }
4798
+
4799
+ while (index < lines.length) {
4800
+ if (isBlankLine(index)) {
4801
+ index += 1;
4802
+ continue;
4803
+ }
4804
+
4805
+ if (isHtmlCommentStart(index)) {
4806
+ let endComment = index;
4807
+ while (endComment < lines.length && getLine(endComment).indexOf("-->") === -1) {
4808
+ endComment += 1;
4809
+ }
4810
+ index = Math.min(lines.length, endComment + 1);
4811
+ continue;
4812
+ }
4813
+
4814
+ if (isThematicBreakLine(index)) {
4815
+ index += 1;
4816
+ continue;
4817
+ }
4818
+
4819
+ const fenceMatch = lineStartsFence(index);
4820
+ if (fenceMatch) {
4821
+ const marker = fenceMatch[1] || "";
4822
+ const markerChar = marker[0] || "`";
4823
+ const markerLength = marker.length;
4824
+ let endFence = index;
4825
+ for (let i = index + 1; i < lines.length; i += 1) {
4826
+ const closingMatch = getLine(i).match(/^ {0,3}(`{3,}|~{3,})\s*$/);
4827
+ if (closingMatch && closingMatch[1] && closingMatch[1][0] === markerChar && closingMatch[1].length >= markerLength) {
4828
+ endFence = i;
4829
+ break;
4830
+ }
4831
+ endFence = i;
4832
+ }
4833
+ blocks.push(makeBlock("code", index, endFence));
4834
+ index = endFence + 1;
4835
+ continue;
4836
+ }
4837
+
4838
+ if (isAtxHeadingLine(index)) {
4839
+ blocks.push(makeBlock("heading", index, index));
4840
+ index += 1;
4841
+ continue;
4842
+ }
4843
+
4844
+ if (!isBlankLine(index) && index + 1 < lines.length && isSetextUnderlineLine(index + 1)) {
4845
+ blocks.push(makeBlock("heading", index, index + 1));
4846
+ index += 2;
4847
+ continue;
4848
+ }
4849
+
4850
+ if (isPotentialTableRow(index) && index + 1 < lines.length && isTableDividerLine(index + 1)) {
4851
+ let endTable = index + 1;
4852
+ for (let i = index + 2; i < lines.length; i += 1) {
4853
+ if (isBlankLine(i) || !isPotentialTableRow(i)) break;
4854
+ endTable = i;
4855
+ }
4856
+ blocks.push(makeBlock("table", index, endTable));
4857
+ index = endTable + 1;
4858
+ continue;
4859
+ }
4860
+
4861
+ if (isBlockquoteLine(index)) {
4862
+ let endQuote = index;
4863
+ for (let i = index + 1; i < lines.length; i += 1) {
4864
+ if (isBlockquoteLine(i)) {
4865
+ endQuote = i;
4866
+ continue;
4867
+ }
4868
+ if (isBlankLine(i) && i + 1 < lines.length && isBlockquoteLine(i + 1)) {
4869
+ endQuote = i;
4870
+ continue;
4871
+ }
4872
+ break;
4873
+ }
4874
+ blocks.push(makeBlock("blockquote", index, endQuote));
4875
+ index = endQuote + 1;
4876
+ continue;
4877
+ }
4878
+
4879
+ if (isListLine(index)) {
4880
+ let endList = index;
4881
+ for (let i = index + 1; i < lines.length; i += 1) {
4882
+ if (isBlankLine(i)) {
4883
+ if (i + 1 < lines.length && (isListLine(i + 1) || isContinuationIndentedLine(i + 1))) {
4884
+ endList = i;
4885
+ continue;
4886
+ }
4887
+ break;
4888
+ }
4889
+ if (isListLine(i) || isContinuationIndentedLine(i)) {
4890
+ endList = i;
4891
+ continue;
4892
+ }
4893
+ if (isAtxHeadingLine(i) || isBlockquoteLine(i) || lineStartsFence(i) || (isPotentialTableRow(i) && i + 1 < lines.length && isTableDividerLine(i + 1))) {
4894
+ break;
4895
+ }
4896
+ endList = i;
4897
+ }
4898
+ blocks.push(makeBlock("list", index, endList));
4899
+ index = endList + 1;
4900
+ continue;
4901
+ }
4902
+
4903
+ let endParagraph = index;
4904
+ for (let i = index + 1; i < lines.length; i += 1) {
4905
+ if (isBlankLine(i) || isHtmlCommentStart(i) || lineStartsFence(i) || isAtxHeadingLine(i) || isBlockquoteLine(i) || isListLine(i)) {
4906
+ break;
4907
+ }
4908
+ if (i + 1 < lines.length && (isSetextUnderlineLine(i + 1) || (isPotentialTableRow(i) && isTableDividerLine(i + 1)))) {
4909
+ break;
4910
+ }
4911
+ endParagraph = i;
4912
+ }
4913
+ blocks.push(makeBlock("paragraph", index, endParagraph));
4914
+ index = endParagraph + 1;
4915
+ }
4916
+
4917
+ return blocks;
4918
+ }
4919
+
4920
+ function getPreviewCommentTargetKind(element) {
4921
+ if (!element || !(element instanceof Element)) return "";
4922
+ const tag = element.tagName ? element.tagName.toUpperCase() : "";
4923
+ if (/^H[1-6]$/.test(tag)) return "heading";
4924
+ if (tag === "P") return "paragraph";
4925
+ if (tag === "BLOCKQUOTE") return "blockquote";
4926
+ if (tag === "UL" || tag === "OL") return "list";
4927
+ if (tag === "TABLE") return "table";
4928
+ if (tag === "PRE") return "code";
4929
+ if (element.classList) {
4930
+ if (
4931
+ element.classList.contains("sourceCode")
4932
+ || element.classList.contains("mermaid-container")
4933
+ ) {
4934
+ return "code";
4935
+ }
4936
+ if (
4937
+ element.classList.contains("callout-note")
4938
+ || element.classList.contains("callout-tip")
4939
+ || element.classList.contains("callout-warning")
4940
+ || element.classList.contains("callout-important")
4941
+ || element.classList.contains("callout-caution")
4942
+ ) {
4943
+ return "blockquote";
4944
+ }
4945
+ }
4946
+ return "";
4947
+ }
4948
+
4949
+ function isPreviewCommentTargetElement(element) {
4950
+ return Boolean(getPreviewCommentTargetKind(element));
4951
+ }
4952
+
4953
+ function collectPreviewCommentTargetElements(targetEl) {
4954
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
4955
+ 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";
4956
+ return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
4957
+ if (!isPreviewCommentTargetElement(element)) return false;
4958
+ let ancestor = element.parentElement;
4959
+ while (ancestor && ancestor !== targetEl) {
4960
+ if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
4961
+ if (isPreviewCommentTargetElement(ancestor)) return false;
4962
+ ancestor = ancestor.parentElement;
4963
+ }
4964
+ return true;
4965
+ }).map((element) => ({
4966
+ element,
4967
+ kind: getPreviewCommentTargetKind(element),
4968
+ }));
4969
+ }
4970
+
4971
+ function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
4972
+ if (!sourceBlock) return "";
4973
+ const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
4974
+ if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
4975
+ return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
4976
+ }
4977
+ if (sourceBlock.kind === "code") {
4978
+ return normalizeVisiblePreviewText(
4979
+ blockText
4980
+ .replace(/^ {0,3}(`{3,}|~{3,}).*$/gm, "")
4981
+ .replace(/^ {0,3}$/gm, ""),
4982
+ );
4983
+ }
4984
+ if (sourceBlock.kind === "table") {
4985
+ return normalizeVisiblePreviewText(
4986
+ blockText
4987
+ .replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/gm, "")
4988
+ .replace(/\|/g, " "),
4989
+ );
4990
+ }
4991
+ return normalizeVisiblePreviewText(blockText);
4992
+ }
4993
+
4994
+ function getNormalizedPreviewCommentTargetText(targetEntry) {
4995
+ if (!targetEntry) return "";
4996
+ if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
4997
+ targetEntry.normalizedText = normalizeVisiblePreviewText(
4998
+ targetEntry.element && typeof targetEntry.element.textContent === "string"
4999
+ ? targetEntry.element.textContent
5000
+ : "",
5001
+ );
5002
+ return targetEntry.normalizedText;
5003
+ }
5004
+
5005
+ function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5006
+ const desiredKind = sourceBlock ? sourceBlock.kind : "";
5007
+ const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
5008
+ let fallbackIndex = -1;
5009
+ let containsIndex = -1;
5010
+
5011
+ for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
5012
+ const targetEntry = targetBlocks[i];
5013
+ if (!targetEntry || targetEntry.kind !== desiredKind) continue;
5014
+ if (fallbackIndex < 0) fallbackIndex = i;
5015
+ const targetText = getNormalizedPreviewCommentTargetText(targetEntry);
5016
+ if (desiredText && targetText) {
5017
+ if (targetText === desiredText) {
5018
+ return i;
5019
+ }
5020
+ if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
5021
+ containsIndex = i;
5022
+ }
5023
+ }
5024
+ }
5025
+
5026
+ if (containsIndex >= 0) return containsIndex;
5027
+ return fallbackIndex;
5028
+ }
5029
+
5030
+ function getPreviewCommentNotesForRange(start, end, sourceText, displayNotes) {
5031
+ const source = String(sourceText || "");
5032
+ const notes = Array.isArray(displayNotes) ? displayNotes : getDisplayReviewNotes();
5033
+ return notes.filter((note) => {
5034
+ const range = resolveReviewNoteRange(note, source);
5035
+ return range && rangesOverlap(range.start, range.end, start, end);
5036
+ });
5037
+ }
5038
+
5039
+ function updatePreviewCommentBlockState(blockEl, sourceText, displayNotes) {
5040
+ if (!blockEl || !blockEl.dataset) return;
5041
+ const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5042
+ const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5043
+ const summaryBtn = blockEl.querySelector(".preview-comment-summary");
5044
+ const addBtn = blockEl.querySelector(".preview-comment-add");
5045
+ const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
5046
+ const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
5047
+ const blockKey = getPreviewCommentBlockKey(blockEl);
5048
+ const hasSelection = Boolean(activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey);
5049
+
5050
+ blockEl.classList.remove("has-comments");
5051
+ blockEl.classList.toggle("has-selection", hasSelection);
5052
+
5053
+ if (summaryBtn) {
5054
+ summaryBtn.hidden = true;
5055
+ summaryBtn.textContent = "";
5056
+ summaryBtn.dataset.reviewNoteId = "";
5057
+ }
5058
+
5059
+ if (addBtn) {
5060
+ addBtn.hidden = !hasSelection;
5061
+ addBtn.textContent = "Comment";
5062
+ addBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
5063
+ addBtn.title = hasSelection
5064
+ ? ("Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").")
5065
+ : "";
5066
+ addBtn.setAttribute("aria-label", addBtn.title || "Comment");
5067
+ }
5068
+ }
5069
+
5070
+ function updatePreviewCommentBlocksForElement(targetEl) {
5071
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5072
+ const sourceText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5073
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5074
+ updatePreviewCommentBlockState(blockEl, sourceText);
5075
+ });
5076
+ }
5077
+
5078
+ function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5079
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5080
+ const sourceBlocks = scanMarkdownPreviewCommentBlocks(sourceText);
5081
+ const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5082
+ if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
5083
+
5084
+ let targetIndex = 0;
5085
+ for (const sourceBlock of sourceBlocks) {
5086
+ const matchedTargetIndex = findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, targetIndex);
5087
+ if (matchedTargetIndex < 0) continue;
5088
+
5089
+ const targetEntry = targetBlocks[matchedTargetIndex];
5090
+ targetIndex = matchedTargetIndex + 1;
5091
+ const originalElement = targetEntry && targetEntry.element ? targetEntry.element : null;
5092
+ if (!originalElement || !originalElement.parentNode) continue;
5093
+
5094
+ const wrapper = document.createElement("div");
5095
+ wrapper.className = "preview-comment-block";
5096
+ wrapper.dataset.reviewNoteStart = String(sourceBlock.start);
5097
+ wrapper.dataset.reviewNoteEnd = String(sourceBlock.end);
5098
+ wrapper.dataset.reviewNoteLineStart = String(sourceBlock.lineStart);
5099
+ wrapper.dataset.reviewNoteLineEnd = String(sourceBlock.lineEnd);
5100
+ wrapper.dataset.previewCommentKind = sourceBlock.kind;
5101
+
5102
+ const controls = document.createElement("div");
5103
+ controls.className = "preview-comment-controls";
5104
+
5105
+ const summaryBtn = document.createElement("button");
5106
+ summaryBtn.type = "button";
5107
+ summaryBtn.className = "preview-comment-summary";
5108
+ summaryBtn.hidden = true;
5109
+ controls.appendChild(summaryBtn);
5110
+
5111
+ const addBtn = document.createElement("button");
5112
+ addBtn.type = "button";
5113
+ addBtn.className = "preview-comment-add";
5114
+ addBtn.textContent = "Comment";
5115
+ controls.appendChild(addBtn);
5116
+
5117
+ originalElement.replaceWith(wrapper);
5118
+ wrapper.appendChild(controls);
5119
+ originalElement.classList.add("preview-comment-block-content");
5120
+ wrapper.appendChild(originalElement);
5121
+ }
5122
+
5123
+ updatePreviewCommentBlocksForElement(targetEl);
5124
+ }
5125
+
5126
+ function refreshRenderedEditorPreviewComments() {
5127
+ if (sourcePreviewEl && !sourcePreviewEl.hidden) {
5128
+ updatePreviewCommentBlocksForElement(sourcePreviewEl);
5129
+ }
5130
+ if (critiqueViewEl && rightView === "editor-preview") {
5131
+ updatePreviewCommentBlocksForElement(critiqueViewEl);
5132
+ }
5133
+ }
5134
+
5135
+ function buildReviewNoteAnchorFromPreviewBlock(blockEl) {
5136
+ if (!blockEl || !blockEl.dataset) return null;
5137
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5138
+ const selectionStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5139
+ const selectionEnd = Math.max(selectionStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || selectionStart, source.length));
5140
+ const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5141
+ const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5142
+ return {
5143
+ selectionStart,
5144
+ selectionEnd,
5145
+ lineStart,
5146
+ lineEnd,
5147
+ selectedText: source.slice(selectionStart, selectionEnd),
5148
+ selectedDisplayText: source.slice(selectionStart, selectionEnd),
5149
+ };
5150
+ }
5151
+
5152
+ function buildReviewNoteAnchorFromPreviewSelection(blockEl, contentEl, range) {
5153
+ if (!blockEl || !blockEl.dataset || !contentEl || !range) return null;
5154
+ const kind = String(blockEl.dataset.previewCommentKind || "");
5155
+ if (!supportsPreviewSelectionCommentsForBlockKind(kind)) return null;
5156
+ if (!contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) return null;
5157
+
5158
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5159
+ const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5160
+ const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5161
+ if (blockEnd <= blockStart) return null;
5162
+
5163
+ const sourceBlockText = source.slice(blockStart, blockEnd);
5164
+ const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5165
+ if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
5166
+
5167
+ const prefixRange = document.createRange();
5168
+ prefixRange.selectNodeContents(contentEl);
5169
+ prefixRange.setEnd(range.startContainer, range.startOffset);
5170
+ const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
5171
+ const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
5172
+ if (!selectedDisplayText) return null;
5173
+
5174
+ const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
5175
+ const bestIndex = findPreferredNormalizedTextMatch(displayMap.text, selectedDisplayText, desiredStart);
5176
+ if (bestIndex < 0) return null;
5177
+
5178
+ const endIndex = bestIndex + selectedDisplayText.length - 1;
5179
+ const rawStartRel = displayMap.charStarts[bestIndex];
5180
+ const rawEndRel = displayMap.charEnds[endIndex];
5181
+ if (!Number.isFinite(rawStartRel) || !Number.isFinite(rawEndRel) || rawEndRel <= rawStartRel) {
5182
+ return null;
5183
+ }
5184
+
5185
+ const selectionStart = blockStart + rawStartRel;
5186
+ const selectionEnd = blockStart + rawEndRel;
5187
+ return {
5188
+ selectionStart,
5189
+ selectionEnd,
5190
+ lineStart: getLineNumberAtOffset(source, selectionStart),
5191
+ lineEnd: getLineNumberAtOffset(source, Math.max(selectionStart, selectionEnd - 1)),
5192
+ selectedText: source.slice(selectionStart, selectionEnd),
5193
+ selectedDisplayText,
5194
+ };
5195
+ }
5196
+
5197
+ function getPreviewJumpNormalizedSelectionStart(note, blockEl, range) {
5198
+ if (!note || !blockEl || !blockEl.dataset || !range) return 0;
5199
+ const kind = String(blockEl.dataset.previewCommentKind || "");
5200
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5201
+ const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5202
+ const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5203
+ const displayMap = buildPreviewSelectionDisplayMap(source.slice(blockStart, blockEnd), kind);
5204
+ if (!displayMap || !displayMap.charStarts || displayMap.charStarts.length === 0) return 0;
5205
+ const relativeStart = Math.max(0, range.start - blockStart);
5206
+ for (let i = 0; i < displayMap.charStarts.length; i += 1) {
5207
+ const charStart = Number(displayMap.charStarts[i]);
5208
+ const charEnd = Number(displayMap.charEnds[i]);
5209
+ if (charEnd > relativeStart && charStart <= relativeStart) {
5210
+ return i;
5211
+ }
5212
+ if (charStart >= relativeStart) {
5213
+ return i;
5214
+ }
5215
+ }
5216
+ return Math.max(0, displayMap.text.length - 1);
5217
+ }
5218
+
5219
+ function createPreviewJumpInlineHighlight(contentEl, blockEl, note, range) {
5220
+ if (!contentEl || !note || !range) return null;
5221
+ const selectedDisplayText = normalizeVisiblePreviewText(note.selectedDisplayText || note.selectedText || "");
5222
+ if (!selectedDisplayText) return null;
5223
+ const domMap = buildNormalizedDomTextMap(contentEl);
5224
+ if (!domMap.text || !domMap.charStarts.length || !domMap.charEnds.length) return null;
5225
+ const preferredStart = getPreviewJumpNormalizedSelectionStart(note, blockEl, range);
5226
+ const matchIndex = findPreferredNormalizedTextMatch(domMap.text, selectedDisplayText, preferredStart);
5227
+ if (matchIndex < 0) return null;
5228
+ const endIndex = matchIndex + selectedDisplayText.length - 1;
5229
+ const startRef = domMap.charStarts[matchIndex];
5230
+ const endRef = domMap.charEnds[endIndex];
5231
+ if (!startRef || !endRef || !startRef.node || !endRef.node) return null;
5232
+
5233
+ const domRange = document.createRange();
5234
+ domRange.setStart(startRef.node, startRef.offset);
5235
+ domRange.setEnd(endRef.node, endRef.offset);
5236
+
5237
+ const highlightEl = document.createElement("span");
5238
+ highlightEl.className = "preview-comment-inline-highlight";
5239
+ try {
5240
+ domRange.surroundContents(highlightEl);
5241
+ } catch {
5242
+ const fragment = domRange.extractContents();
5243
+ highlightEl.appendChild(fragment);
5244
+ domRange.insertNode(highlightEl);
5245
+ }
5246
+ return highlightEl;
5247
+ }
5248
+
5249
+ function findPreviewCommentBlockForRange(targetEl, range) {
5250
+ if (!targetEl || !range || typeof targetEl.querySelectorAll !== "function") return null;
5251
+ let bestBlock = null;
5252
+ let bestScore = Number.NEGATIVE_INFINITY;
5253
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5254
+ const blockStart = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
5255
+ const blockEnd = Math.max(blockStart, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || blockStart);
5256
+ const overlapStart = Math.max(blockStart, range.start);
5257
+ const overlapEnd = Math.min(blockEnd, range.end);
5258
+ const overlap = Math.max(0, overlapEnd - overlapStart);
5259
+ const contains = range.start >= blockStart && range.end <= blockEnd;
5260
+ const distance = contains
5261
+ ? 0
5262
+ : Math.min(Math.abs(range.start - blockEnd), Math.abs(range.end - blockStart));
5263
+ const score = contains
5264
+ ? (1000000 - (blockEnd - blockStart))
5265
+ : (overlap > 0 ? overlap : -distance);
5266
+ if (score > bestScore) {
5267
+ bestScore = score;
5268
+ bestBlock = blockEl;
5269
+ }
5270
+ });
5271
+ return bestBlock;
5272
+ }
5273
+
5274
+ function revealReviewNoteInPreviewElement(targetEl, note) {
5275
+ if (!targetEl || !note) return false;
5276
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5277
+ const range = resolveReviewNoteRange(note, source);
5278
+ if (!range) return false;
5279
+ const blockEl = findPreviewCommentBlockForRange(targetEl, range);
5280
+ if (!blockEl) return false;
5281
+ const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5282
+ const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
5283
+ if (typeof blockEl.scrollIntoView === "function") {
5284
+ blockEl.scrollIntoView({ block: "center", inline: "nearest" });
5285
+ }
5286
+ setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl);
5287
+ return true;
5288
+ }
5289
+
5290
+ function revealReviewNoteInPreview(note) {
5291
+ if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
5292
+ revealReviewNoteInPreviewElement(critiqueViewEl, note);
5293
+ }
5294
+ }
5295
+
5296
+ function updateActivePreviewCommentSelectionFromDom() {
5297
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
5298
+ if (!selection || selection.rangeCount <= 0 || selection.isCollapsed) {
5299
+ clearPreviewCommentSelection();
5300
+ return;
5301
+ }
5302
+
5303
+ const range = selection.getRangeAt(0);
5304
+ const startBlock = findPreviewCommentBlockFromNode(range.startContainer);
5305
+ const endBlock = findPreviewCommentBlockFromNode(range.endContainer);
5306
+ if (!startBlock || !endBlock || startBlock !== endBlock) {
5307
+ clearPreviewCommentSelection();
5308
+ return;
5309
+ }
5310
+
5311
+ const contentEl = startBlock.querySelector(".preview-comment-block-content");
5312
+ if (!contentEl || !contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) {
5313
+ clearPreviewCommentSelection();
5314
+ return;
5315
+ }
5316
+
5317
+ const anchor = buildReviewNoteAnchorFromPreviewSelection(startBlock, contentEl, range);
5318
+ if (!anchor) {
5319
+ clearPreviewCommentSelection();
5320
+ return;
5321
+ }
5322
+
5323
+ setActivePreviewCommentSelection({
5324
+ ...anchor,
5325
+ blockKey: getPreviewCommentBlockKey(startBlock),
5326
+ });
5327
+ }
5328
+
4317
5329
  function getDisplayReviewNotes() {
4318
5330
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
4319
5331
  return reviewNotes.slice().sort((left, right) => {
@@ -4386,6 +5398,7 @@
4386
5398
  reviewNotes = cloneReviewNotes(nextNotes);
4387
5399
  updateReviewNotesUi();
4388
5400
  renderReviewNotesList();
5401
+ refreshRenderedEditorPreviewComments();
4389
5402
  if (editorView === "markdown") {
4390
5403
  scheduleEditorLineNumberRender();
4391
5404
  }
@@ -4394,6 +5407,31 @@
4394
5407
  }
4395
5408
  }
4396
5409
 
5410
+ function updateEditorSelectionCommentUi() {
5411
+ if (!editorSelectionCommentBtn) return;
5412
+ const hasSelection = Boolean(
5413
+ !suppressEditorSelectionComment
5414
+ && editorView === "markdown"
5415
+ && document.activeElement === sourceTextEl
5416
+ && typeof sourceTextEl.selectionStart === "number"
5417
+ && typeof sourceTextEl.selectionEnd === "number"
5418
+ && sourceTextEl.selectionEnd > sourceTextEl.selectionStart
5419
+ );
5420
+ editorSelectionCommentBtn.hidden = !hasSelection;
5421
+ if (hasSelection) {
5422
+ editorSelectionCommentBtn.title = "Create a new local comment from the current editor selection.";
5423
+ editorSelectionCommentBtn.setAttribute("aria-label", editorSelectionCommentBtn.title);
5424
+ }
5425
+ }
5426
+
5427
+ function clearSuppressedEditorSelectionComment() {
5428
+ if (!suppressEditorSelectionComment) return;
5429
+ suppressEditorSelectionComment = false;
5430
+ suppressedEditorSelectionStart = null;
5431
+ suppressedEditorSelectionEnd = null;
5432
+ updateEditorSelectionCommentUi();
5433
+ }
5434
+
4397
5435
  function updateReviewNotesUi() {
4398
5436
  const descriptor = getCurrentStudioDocumentDescriptor();
4399
5437
  const count = reviewNotes.length;
@@ -4421,8 +5459,10 @@
4421
5459
  if (reviewNotesAddBtn) {
4422
5460
  reviewNotesAddBtn.disabled = editorView !== "markdown";
4423
5461
  reviewNotesAddBtn.title = editorView === "markdown"
4424
- ? "Create a new local comment from the current editor selection, or from the current line if nothing is selected."
4425
- : "Switch to Editor (Raw) to anchor a comment to the current selection or line.";
5462
+ ? "Create a new local comment on the current editor line."
5463
+ : (supportsPreviewCommentsForCurrentEditor()
5464
+ ? "Select preview text and use Comment for a local preview-anchored comment."
5465
+ : "Switch to Editor (Raw) to comment on the current line.");
4426
5466
  }
4427
5467
  if (reviewNotesInlineAllBtn) {
4428
5468
  const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
@@ -4580,12 +5620,49 @@
4580
5620
  pendingReviewNoteInlineFocusId = null;
4581
5621
  }
4582
5622
 
4583
- function addReviewNoteFromEditorSelection() {
4584
- if (editorView !== "markdown") {
4585
- setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
4586
- return;
5623
+ function focusReviewNotesForPreviewBlock(blockEl) {
5624
+ if (!blockEl) return;
5625
+ const start = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
5626
+ const end = Math.max(start, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || start);
5627
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5628
+ const notes = getPreviewCommentNotesForRange(start, end, source);
5629
+ if (!notes.length) return;
5630
+ focusReviewNoteInPanel(notes[0].id);
5631
+ }
5632
+
5633
+ function addReviewNoteFromPreviewBlock(blockEl) {
5634
+ const anchor = buildReviewNoteAnchorFromPreviewBlock(blockEl);
5635
+ if (!anchor) return null;
5636
+ return addReviewNoteFromAnchor(anchor, {
5637
+ statusMessage: "Added local comment from editor preview.",
5638
+ });
5639
+ }
5640
+
5641
+ function addReviewNoteFromPreviewSelection(blockEl) {
5642
+ if (!blockEl) return null;
5643
+ const blockKey = getPreviewCommentBlockKey(blockEl);
5644
+ const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
5645
+ ? activePreviewCommentSelection
5646
+ : null;
5647
+ if (!anchor) {
5648
+ setStatus("Select some preview text within a single block first.", "warning");
5649
+ return null;
5650
+ }
5651
+ const note = addReviewNoteFromAnchor(anchor, {
5652
+ statusMessage: "Added local comment from preview selection.",
5653
+ });
5654
+ if (note) {
5655
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
5656
+ if (selection && typeof selection.removeAllRanges === "function") {
5657
+ selection.removeAllRanges();
5658
+ }
5659
+ clearPreviewCommentSelection();
4587
5660
  }
4588
- const anchor = getEditorAnchorForReviewNote();
5661
+ return note;
5662
+ }
5663
+
5664
+ function addReviewNoteFromAnchor(anchor, options) {
5665
+ if (!anchor || typeof anchor !== "object") return null;
4589
5666
  const note = normalizeReviewNote({
4590
5667
  id: makeRequestId(),
4591
5668
  text: "",
@@ -4596,14 +5673,49 @@
4596
5673
  lineStart: anchor.lineStart,
4597
5674
  lineEnd: anchor.lineEnd,
4598
5675
  selectedText: anchor.selectedText,
5676
+ selectedDisplayText: typeof anchor.selectedDisplayText === "string" ? anchor.selectedDisplayText : (typeof anchor.selectedText === "string" ? anchor.selectedText : ""),
4599
5677
  });
4600
- if (!note) return;
5678
+ if (!note) return null;
5679
+ if (editorSelectionCommentBtn) {
5680
+ editorSelectionCommentBtn.hidden = true;
5681
+ }
5682
+ const shouldOpenReviewNotes = !isReviewNotesOpen();
4601
5683
  pendingReviewNoteFocusId = note.id;
4602
5684
  setReviewNotes(reviewNotes.concat([note]));
4603
- if (!isReviewNotesOpen()) {
5685
+ if (shouldOpenReviewNotes) {
5686
+ pendingReviewNoteFocusId = note.id;
4604
5687
  openReviewNotes();
4605
5688
  }
4606
- setStatus("Added local comment.", "success");
5689
+ const schedule = typeof window.requestAnimationFrame === "function"
5690
+ ? window.requestAnimationFrame.bind(window)
5691
+ : (cb) => window.setTimeout(cb, 16);
5692
+ schedule(() => {
5693
+ updateEditorSelectionCommentUi();
5694
+ });
5695
+ if (!options || options.status !== false) {
5696
+ setStatus((options && options.statusMessage) || "Added local comment.", "success");
5697
+ }
5698
+ return note;
5699
+ }
5700
+
5701
+ function addReviewNoteFromEditorSelection() {
5702
+ if (editorView !== "markdown") {
5703
+ setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
5704
+ return;
5705
+ }
5706
+ addReviewNoteFromAnchor(getEditorAnchorForReviewNote(), {
5707
+ statusMessage: "Added local comment.",
5708
+ });
5709
+ }
5710
+
5711
+ function addReviewNoteFromEditorLine() {
5712
+ if (editorView !== "markdown") {
5713
+ setStatus("Switch to Editor (Raw) before adding a line comment.", "warning");
5714
+ return;
5715
+ }
5716
+ addReviewNoteFromAnchor(getEditorLineAnchorForReviewNote(), {
5717
+ statusMessage: "Added local line comment.",
5718
+ });
4607
5719
  }
4608
5720
 
4609
5721
  function jumpToReviewNote(noteId) {
@@ -4615,6 +5727,10 @@
4615
5727
  setStatus("Could not find the anchored location for this comment.", "warning");
4616
5728
  return;
4617
5729
  }
5730
+ suppressEditorSelectionComment = true;
5731
+ suppressedEditorSelectionStart = range.start;
5732
+ suppressedEditorSelectionEnd = range.end;
5733
+ updateEditorSelectionCommentUi();
4618
5734
  setEditorView("markdown");
4619
5735
  setActivePane("left");
4620
5736
  sourceTextEl.focus();
@@ -4624,6 +5740,8 @@
4624
5740
  : (cb) => window.setTimeout(cb, 16);
4625
5741
  schedule(() => {
4626
5742
  scrollEditorRangeIntoView(range);
5743
+ revealReviewNoteInPreview(note);
5744
+ updateEditorSelectionCommentUi();
4627
5745
  });
4628
5746
  }
4629
5747
 
@@ -6116,14 +7234,52 @@
6116
7234
  });
6117
7235
 
6118
7236
  sourceTextEl.addEventListener("input", () => {
7237
+ if (activePreviewCommentSelection) {
7238
+ clearPreviewCommentSelection();
7239
+ }
7240
+ clearSuppressedEditorSelectionComment();
6119
7241
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
6120
7242
  scheduleEditorMetaUpdate();
7243
+ updateEditorSelectionCommentUi();
6121
7244
  if (isReviewNotesOpen() && reviewNotes.length > 0) {
6122
7245
  renderReviewNotesList();
6123
7246
  updateReviewNotesUi();
6124
7247
  }
6125
7248
  });
6126
7249
 
7250
+ sourceTextEl.addEventListener("select", () => {
7251
+ if (suppressEditorSelectionComment) {
7252
+ const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
7253
+ const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
7254
+ const matchesSuppressedSelection = selectionStart === suppressedEditorSelectionStart && selectionEnd === suppressedEditorSelectionEnd;
7255
+ if (!matchesSuppressedSelection && selectionEnd > selectionStart) {
7256
+ clearSuppressedEditorSelectionComment();
7257
+ }
7258
+ }
7259
+ updateEditorSelectionCommentUi();
7260
+ });
7261
+
7262
+ sourceTextEl.addEventListener("keyup", () => {
7263
+ updateEditorSelectionCommentUi();
7264
+ });
7265
+
7266
+ sourceTextEl.addEventListener("mouseup", () => {
7267
+ updateEditorSelectionCommentUi();
7268
+ });
7269
+
7270
+ sourceTextEl.addEventListener("focus", () => {
7271
+ updateEditorSelectionCommentUi();
7272
+ });
7273
+
7274
+ sourceTextEl.addEventListener("blur", () => {
7275
+ const schedule = typeof window.requestAnimationFrame === "function"
7276
+ ? window.requestAnimationFrame.bind(window)
7277
+ : (cb) => window.setTimeout(cb, 16);
7278
+ schedule(() => {
7279
+ updateEditorSelectionCommentUi();
7280
+ });
7281
+ });
7282
+
6127
7283
  sourceTextEl.addEventListener("scroll", () => {
6128
7284
  if (editorView !== "markdown") return;
6129
7285
  syncEditorHighlightScroll();
@@ -6487,6 +7643,15 @@
6487
7643
 
6488
7644
  if (reviewNotesAddBtn) {
6489
7645
  reviewNotesAddBtn.addEventListener("click", () => {
7646
+ addReviewNoteFromEditorLine();
7647
+ });
7648
+ }
7649
+
7650
+ if (editorSelectionCommentBtn) {
7651
+ editorSelectionCommentBtn.addEventListener("mousedown", (event) => {
7652
+ event.preventDefault();
7653
+ });
7654
+ editorSelectionCommentBtn.addEventListener("click", () => {
6490
7655
  addReviewNoteFromEditorSelection();
6491
7656
  });
6492
7657
  }
@@ -6508,6 +7673,42 @@
6508
7673
  });
6509
7674
  }
6510
7675
 
7676
+ function handlePreviewCommentActionMouseDown(event) {
7677
+ const target = event.target;
7678
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
7679
+ if (!actionBtn) return;
7680
+ event.preventDefault();
7681
+ }
7682
+
7683
+ function handlePreviewCommentActionClick(event) {
7684
+ const target = event.target;
7685
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
7686
+ if (!actionBtn) return;
7687
+ const blockEl = actionBtn.closest(".preview-comment-block");
7688
+ if (!blockEl) return;
7689
+ event.preventDefault();
7690
+ event.stopPropagation();
7691
+ const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
7692
+ if (mode !== "selection") return;
7693
+ addReviewNoteFromPreviewSelection(blockEl);
7694
+ }
7695
+
7696
+ if (leftPaneEl) {
7697
+ leftPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
7698
+ leftPaneEl.addEventListener("click", handlePreviewCommentActionClick);
7699
+ }
7700
+
7701
+ if (rightPaneEl) {
7702
+ rightPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
7703
+ rightPaneEl.addEventListener("click", handlePreviewCommentActionClick);
7704
+ }
7705
+
7706
+ if (typeof document.addEventListener === "function") {
7707
+ document.addEventListener("selectionchange", () => {
7708
+ updateActivePreviewCommentSelectionFromDom();
7709
+ });
7710
+ }
7711
+
6511
7712
  if (scratchpadBtn) {
6512
7713
  scratchpadBtn.addEventListener("click", () => {
6513
7714
  openScratchpad();