pi-studio 0.5.49 → 0.5.51

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,7 +53,9 @@
53
53
  const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
54
54
  const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
55
55
  const sourcePreviewEl = document.getElementById("sourcePreview");
56
+ const editorSelectionActionsEl = document.getElementById("editorSelectionActions");
56
57
  const editorSelectionCommentBtn = document.getElementById("editorSelectionCommentBtn");
58
+ const editorSelectionJumpBtn = document.getElementById("editorSelectionJumpBtn");
57
59
  const leftPaneEl = document.getElementById("leftPane");
58
60
  const rightPaneEl = document.getElementById("rightPane");
59
61
  const sourceBadgeEl = document.getElementById("sourceBadge");
@@ -1904,6 +1906,7 @@
1904
1906
 
1905
1907
  fallbackTargets.forEach((entry) => {
1906
1908
  entry.renderTarget.classList.add("studio-mathjax-fallback");
1909
+ entry.renderTarget.setAttribute("data-tex-source", entry.tex);
1907
1910
  if (entry.displayMode) {
1908
1911
  entry.renderTarget.classList.add("studio-mathjax-fallback-display");
1909
1912
  entry.renderTarget.textContent = "\\[\n" + entry.tex + "\n\\]";
@@ -3858,10 +3861,6 @@
3858
3861
  + " data-review-note-line-end='" + String(lineNumber) + "'"
3859
3862
  + " data-preview-comment-kind='" + escapeHtml(kind) + "'"
3860
3863
  + ">"
3861
- + "<div class='preview-comment-controls'>"
3862
- + "<button type='button' class='preview-comment-summary' hidden></button>"
3863
- + "<button type='button' class='preview-comment-add'>Comment</button>"
3864
- + "</div>"
3865
3864
  + "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
3866
3865
  + "</div>",
3867
3866
  );
@@ -3877,6 +3876,7 @@
3877
3876
  clearPreviewJumpHighlight(targetEl);
3878
3877
  finishPreviewRender(targetEl);
3879
3878
  targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
3879
+ ensurePreviewSelectionActions(targetEl);
3880
3880
  updatePreviewCommentBlocksForElement(targetEl);
3881
3881
  if (pane === "response") {
3882
3882
  applyPendingResponseScrollReset();
@@ -4447,10 +4447,9 @@
4447
4447
  }
4448
4448
 
4449
4449
  function supportsPreviewCommentsForCurrentEditor() {
4450
- // LaTeX preview comments are intentionally disabled for now.
4451
- // The initial client-side source/block heuristics were too unreliable on complex real documents.
4452
- // Keep the independent LaTeX rendering fixes, but only enable preview comments where mapping is robust.
4453
- return editorLanguage === "markdown" || supportsCodePreviewCommentsForCurrentEditor();
4450
+ return editorLanguage === "markdown"
4451
+ || editorLanguage === "latex"
4452
+ || supportsCodePreviewCommentsForCurrentEditor();
4454
4453
  }
4455
4454
 
4456
4455
  function getPreviewCommentBlockKindLabel(kind) {
@@ -4458,6 +4457,8 @@
4458
4457
  if (kind === "blockquote") return "quote block";
4459
4458
  if (kind === "list") return "list";
4460
4459
  if (kind === "math") return "equation";
4460
+ if (kind === "figure") return "figure";
4461
+ if (kind === "algorithm") return "algorithm block";
4461
4462
  if (kind === "page-break") return "page break";
4462
4463
  if (kind === "code") return "code block";
4463
4464
  if (kind === "table") return "table";
@@ -4670,6 +4671,389 @@
4670
4671
  bodyText: String(range.bodyText || ""),
4671
4672
  };
4672
4673
  }
4674
+
4675
+ const LATEX_PREVIEW_HEADING_COMMANDS = new Set([
4676
+ "part",
4677
+ "chapter",
4678
+ "section",
4679
+ "subsection",
4680
+ "subsubsection",
4681
+ "paragraph",
4682
+ "subparagraph",
4683
+ ]);
4684
+ const LATEX_PREVIEW_VISIBLE_GROUP_COMMANDS = new Set([
4685
+ "part",
4686
+ "chapter",
4687
+ "section",
4688
+ "subsection",
4689
+ "subsubsection",
4690
+ "paragraph",
4691
+ "subparagraph",
4692
+ "title",
4693
+ "author",
4694
+ "caption",
4695
+ "text",
4696
+ "textbf",
4697
+ "textit",
4698
+ "emph",
4699
+ "underline",
4700
+ "texttt",
4701
+ "textrm",
4702
+ "textsf",
4703
+ "textsc",
4704
+ "mbox",
4705
+ "makebox",
4706
+ "framebox",
4707
+ "fbox",
4708
+ "url",
4709
+ "path",
4710
+ "nolinkurl",
4711
+ ]);
4712
+ const LATEX_PREVIEW_SECOND_ARG_VISIBLE_COMMANDS = new Set([
4713
+ "href",
4714
+ "hyperref",
4715
+ ]);
4716
+ const LATEX_PREVIEW_HIDDEN_COMMANDS = new Set([
4717
+ "label",
4718
+ "ref",
4719
+ "eqref",
4720
+ "autoref",
4721
+ "pageref",
4722
+ "cite",
4723
+ "citet",
4724
+ "citep",
4725
+ "citealt",
4726
+ "citeauthor",
4727
+ "nocite",
4728
+ "footnote",
4729
+ "marginpar",
4730
+ "index",
4731
+ "includegraphics",
4732
+ "addbibresource",
4733
+ ]);
4734
+ const LATEX_PREVIEW_SKIPPED_ENV_NAMES = new Set([
4735
+ "document",
4736
+ "thebibliography",
4737
+ "itemize",
4738
+ "enumerate",
4739
+ "description",
4740
+ "figure",
4741
+ "figure*",
4742
+ "table",
4743
+ "table*",
4744
+ "tabular",
4745
+ "tabular*",
4746
+ "theorem",
4747
+ "lemma",
4748
+ "proposition",
4749
+ "corollary",
4750
+ "definition",
4751
+ "proof",
4752
+ "remark",
4753
+ "example",
4754
+ "verbatim",
4755
+ "lstlisting",
4756
+ "minted",
4757
+ "algorithm",
4758
+ "algorithm*",
4759
+ "algorithmic",
4760
+ ]);
4761
+ const LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME = new Map([
4762
+ ["figure", "figure"],
4763
+ ["figure*", "figure"],
4764
+ ["table", "table"],
4765
+ ["table*", "table"],
4766
+ ["algorithm", "algorithm"],
4767
+ ["algorithm*", "algorithm"],
4768
+ ]);
4769
+
4770
+ function stripLatexPreviewComments(text) {
4771
+ const source = String(text || "");
4772
+ let out = "";
4773
+ for (let index = 0; index < source.length; index += 1) {
4774
+ const ch = source[index];
4775
+ if (ch === "%" && !isEscapedAt(source, index)) {
4776
+ while (index < source.length && source[index] !== "\n") index += 1;
4777
+ if (index < source.length && source[index] === "\n") {
4778
+ out += "\n";
4779
+ }
4780
+ continue;
4781
+ }
4782
+ out += ch;
4783
+ }
4784
+ return out;
4785
+ }
4786
+
4787
+ function skipLatexPreviewCommentSpace(source, startIndex) {
4788
+ let index = Math.max(0, Number(startIndex) || 0);
4789
+ while (index < source.length) {
4790
+ const ch = source[index];
4791
+ if (/\s/.test(ch)) {
4792
+ index += 1;
4793
+ continue;
4794
+ }
4795
+ if (ch === "%" && !isEscapedAt(source, index)) {
4796
+ while (index < source.length && source[index] !== "\n") index += 1;
4797
+ continue;
4798
+ }
4799
+ break;
4800
+ }
4801
+ return index;
4802
+ }
4803
+
4804
+ function readLatexHeadingChunk(chunkText) {
4805
+ const source = String(chunkText || "");
4806
+ let index = skipLatexPreviewCommentSpace(source, 0);
4807
+ const command = parseLatexCommandAt(source, index);
4808
+ const commandName = command && command.name
4809
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
4810
+ : "";
4811
+ if (!command || !LATEX_PREVIEW_HEADING_COMMANDS.has(commandName)) return null;
4812
+ index = skipLatexPreviewCommentSpace(source, command.end);
4813
+ if (source[index] === "[") {
4814
+ const optionalGroup = readBalancedLatexGroup(source, index, "[", "]");
4815
+ if (optionalGroup) {
4816
+ index = skipLatexPreviewCommentSpace(source, optionalGroup.end);
4817
+ }
4818
+ }
4819
+ if (source[index] !== "{") return null;
4820
+ const titleGroup = readBalancedLatexGroup(source, index, "{", "}");
4821
+ if (!titleGroup) return null;
4822
+ index = skipLatexPreviewCommentSpace(source, titleGroup.end);
4823
+ while (index < source.length) {
4824
+ const trailingCommand = parseLatexCommandAt(source, index);
4825
+ const trailingName = trailingCommand && trailingCommand.name
4826
+ ? String(trailingCommand.name || "").replace(/\*$/, "").toLowerCase()
4827
+ : "";
4828
+ if (!trailingCommand || !LATEX_PREVIEW_HIDDEN_COMMANDS.has(trailingName)) {
4829
+ break;
4830
+ }
4831
+ let nextIndex = skipLatexPreviewCommentSpace(source, trailingCommand.end);
4832
+ if (source[nextIndex] === "[") {
4833
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4834
+ if (optionalGroup) {
4835
+ nextIndex = skipLatexPreviewCommentSpace(source, optionalGroup.end);
4836
+ }
4837
+ }
4838
+ if (source[nextIndex] === "{") {
4839
+ const argGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4840
+ if (argGroup) {
4841
+ nextIndex = skipLatexPreviewCommentSpace(source, argGroup.end);
4842
+ }
4843
+ }
4844
+ index = nextIndex;
4845
+ }
4846
+ if (skipLatexPreviewCommentSpace(source, index) < source.length) return null;
4847
+ return {
4848
+ commandName,
4849
+ titleText: source.slice(titleGroup.contentStart, titleGroup.contentEnd),
4850
+ };
4851
+ }
4852
+
4853
+ function extractLatexPreviewVisibleText(text) {
4854
+ const source = String(text || "");
4855
+ let out = "";
4856
+ let index = 0;
4857
+
4858
+ while (index < source.length) {
4859
+ const ch = source[index];
4860
+ if (ch === "%" && !isEscapedAt(source, index)) {
4861
+ while (index < source.length && source[index] !== "\n") index += 1;
4862
+ continue;
4863
+ }
4864
+ if (source.startsWith("$$", index)) {
4865
+ const close = source.indexOf("$$", index + 2);
4866
+ if (close >= 0) {
4867
+ out += " " + source.slice(index + 2, close) + " ";
4868
+ index = close + 2;
4869
+ continue;
4870
+ }
4871
+ }
4872
+ if (ch === "$" && !isEscapedAt(source, index)) {
4873
+ const close = findClosingUnescapedSequence(source, index + 1, "$", true);
4874
+ if (close >= 0) {
4875
+ out += " " + source.slice(index + 1, close) + " ";
4876
+ index = close + 1;
4877
+ continue;
4878
+ }
4879
+ }
4880
+ if (source.startsWith("\\(", index)) {
4881
+ const close = source.indexOf("\\)", index + 2);
4882
+ if (close >= 0) {
4883
+ out += " " + source.slice(index + 2, close) + " ";
4884
+ index = close + 2;
4885
+ continue;
4886
+ }
4887
+ }
4888
+ if (source.startsWith("\\[", index)) {
4889
+ const close = source.indexOf("\\]", index + 2);
4890
+ if (close >= 0) {
4891
+ out += " " + source.slice(index + 2, close) + " ";
4892
+ index = close + 2;
4893
+ continue;
4894
+ }
4895
+ }
4896
+ if (source.startsWith("\\begin{", index)) {
4897
+ const envGroup = readBalancedLatexGroup(source, index + 6, "{", "}");
4898
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim() : "";
4899
+ if (envName && DISPLAY_MATH_ENV_NAMES.has(envName)) {
4900
+ const closeToken = "\\end{" + envName + "}";
4901
+ const close = source.indexOf(closeToken, envGroup.end);
4902
+ if (close >= 0) {
4903
+ out += " " + source.slice(envGroup.end, close) + " ";
4904
+ index = close + closeToken.length;
4905
+ continue;
4906
+ }
4907
+ }
4908
+ }
4909
+ if (source.startsWith("\\end{", index)) {
4910
+ const envGroup = readBalancedLatexGroup(source, index + 4, "{", "}");
4911
+ if (envGroup) {
4912
+ index = envGroup.end;
4913
+ continue;
4914
+ }
4915
+ }
4916
+ if (ch === "\\") {
4917
+ const command = parseLatexCommandAt(source, index);
4918
+ const commandName = command && command.name
4919
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
4920
+ : "";
4921
+ if (!command) {
4922
+ index += 1;
4923
+ continue;
4924
+ }
4925
+ if (commandName === "begin" || commandName === "end") {
4926
+ let nextIndex = skipLatexWhitespace(source, command.end);
4927
+ if (source[nextIndex] === "{") {
4928
+ const group = readBalancedLatexGroup(source, nextIndex, "{", "}");
4929
+ if (group) {
4930
+ index = group.end;
4931
+ continue;
4932
+ }
4933
+ }
4934
+ }
4935
+ if (commandName === "latex") {
4936
+ out += "LaTeX";
4937
+ index = command.end;
4938
+ continue;
4939
+ }
4940
+ if (commandName === "tex") {
4941
+ out += "TeX";
4942
+ index = command.end;
4943
+ continue;
4944
+ }
4945
+ if (commandName === "item") {
4946
+ out += " ";
4947
+ index = command.end;
4948
+ continue;
4949
+ }
4950
+ let nextIndex = skipLatexWhitespace(source, command.end);
4951
+ if (source[nextIndex] === "[") {
4952
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4953
+ if (optionalGroup) {
4954
+ nextIndex = skipLatexWhitespace(source, optionalGroup.end);
4955
+ }
4956
+ }
4957
+ if (LATEX_PREVIEW_VISIBLE_GROUP_COMMANDS.has(commandName) && source[nextIndex] === "{") {
4958
+ const group = readBalancedLatexGroup(source, nextIndex, "{", "}");
4959
+ if (group) {
4960
+ out += " " + extractLatexPreviewVisibleText(source.slice(group.contentStart, group.contentEnd)) + " ";
4961
+ index = group.end;
4962
+ continue;
4963
+ }
4964
+ }
4965
+ if (LATEX_PREVIEW_SECOND_ARG_VISIBLE_COMMANDS.has(commandName) && source[nextIndex] === "{") {
4966
+ const firstGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4967
+ if (firstGroup) {
4968
+ let secondIndex = skipLatexWhitespace(source, firstGroup.end);
4969
+ if (source[secondIndex] === "{") {
4970
+ const secondGroup = readBalancedLatexGroup(source, secondIndex, "{", "}");
4971
+ if (secondGroup) {
4972
+ out += " " + extractLatexPreviewVisibleText(source.slice(secondGroup.contentStart, secondGroup.contentEnd)) + " ";
4973
+ index = secondGroup.end;
4974
+ continue;
4975
+ }
4976
+ }
4977
+ }
4978
+ }
4979
+ if (LATEX_PREVIEW_HIDDEN_COMMANDS.has(commandName)) {
4980
+ index = nextIndex;
4981
+ if (source[index] === "{") {
4982
+ const group = readBalancedLatexGroup(source, index, "{", "}");
4983
+ if (group) {
4984
+ index = group.end;
4985
+ continue;
4986
+ }
4987
+ }
4988
+ index = command.end;
4989
+ continue;
4990
+ }
4991
+ index = command.end;
4992
+ continue;
4993
+ }
4994
+ if (ch === "{" || ch === "}") {
4995
+ index += 1;
4996
+ continue;
4997
+ }
4998
+ if (ch === "~") {
4999
+ out += " ";
5000
+ index += 1;
5001
+ continue;
5002
+ }
5003
+ out += ch;
5004
+ index += 1;
5005
+ }
5006
+
5007
+ return normalizeVisiblePreviewText(out);
5008
+ }
5009
+
5010
+ function findLatexDocumentBodyRange(text) {
5011
+ const source = String(text || "");
5012
+ const beginMatch = source.match(/\\begin\{document\}/);
5013
+ if (!beginMatch || beginMatch.index == null) {
5014
+ return { start: 0, end: source.length };
5015
+ }
5016
+ const start = beginMatch.index + beginMatch[0].length;
5017
+ const endMatch = source.slice(start).match(/\\end\{document\}/);
5018
+ return {
5019
+ start,
5020
+ end: endMatch && endMatch.index != null ? (start + endMatch.index) : source.length,
5021
+ };
5022
+ }
5023
+
5024
+ function normalizeLatexPreviewBlockText(blockText, kind) {
5025
+ const source = String(blockText || "");
5026
+ if (/\\(?:bibliography|printbibliography)\b/i.test(source)) {
5027
+ return kind === "heading" ? "References" : "references";
5028
+ }
5029
+ if (kind === "math") {
5030
+ const mathRange = getStandaloneDisplayMathRange(stripLatexPreviewComments(source));
5031
+ return mathRange ? normalizeVisiblePreviewText(mathRange.bodyText) : normalizeVisiblePreviewText(source);
5032
+ }
5033
+ if (kind === "heading") {
5034
+ const heading = readLatexHeadingChunk(stripLatexPreviewComments(source));
5035
+ return heading ? extractLatexPreviewVisibleText(heading.titleText) : extractLatexPreviewVisibleText(source);
5036
+ }
5037
+ return extractLatexPreviewVisibleText(source);
5038
+ }
5039
+
5040
+ function isLatexPreviewSkippableChunk(chunkText) {
5041
+ const source = stripLatexPreviewComments(chunkText).trim();
5042
+ if (!source) return true;
5043
+ const command = parseLatexCommandAt(source, 0);
5044
+ const commandName = command && command.name
5045
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
5046
+ : "";
5047
+ if (command && LATEX_PREVIEW_HIDDEN_COMMANDS.has(commandName)) return true;
5048
+ if (command && /^(?:documentclass|usepackage|newtheorem|title|author|date|maketitle|tableofcontents)$/i.test(commandName)) return true;
5049
+ if (source.startsWith("\\begin{")) {
5050
+ const envGroup = readBalancedLatexGroup(source, 6, "{", "}");
5051
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim().toLowerCase() : "";
5052
+ if (envName && LATEX_PREVIEW_SKIPPED_ENV_NAMES.has(envName)) return true;
5053
+ }
5054
+ return false;
5055
+ }
5056
+
4673
5057
  function normalizePreviewComparableCharacter(character) {
4674
5058
  switch (String(character || "")) {
4675
5059
  case "\u2018":
@@ -5102,12 +5486,12 @@
5102
5486
 
5103
5487
  function getPreviewMathSearchText(element) {
5104
5488
  if (!element || !(element instanceof Element)) return null;
5489
+ const texSourceAttr = element.getAttribute("data-tex-source");
5490
+ if (texSourceAttr && texSourceAttr.trim()) {
5491
+ return texSourceAttr;
5492
+ }
5105
5493
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
5106
5494
  if (tag === "MATH") {
5107
- const texSource = element.getAttribute("data-tex-source");
5108
- if (texSource && texSource.trim()) {
5109
- return texSource;
5110
- }
5111
5495
  return typeof element.textContent === "string" ? element.textContent : "";
5112
5496
  }
5113
5497
  if (element.classList && element.classList.contains("math") && (element.classList.contains("inline") || element.classList.contains("display"))) {
@@ -5116,6 +5500,16 @@
5116
5500
  element.classList.contains("display"),
5117
5501
  );
5118
5502
  }
5503
+ if (
5504
+ element.classList
5505
+ && (element.classList.contains("studio-display-equation") || element.classList.contains("studio-display-equation-body"))
5506
+ && typeof element.querySelector === "function"
5507
+ ) {
5508
+ const innerMathEl = element.querySelector("[data-tex-source], math[display='block'], .studio-mathjax-fallback-display");
5509
+ if (innerMathEl && innerMathEl !== element) {
5510
+ return getPreviewMathSearchText(innerMathEl);
5511
+ }
5512
+ }
5119
5513
  return null;
5120
5514
  }
5121
5515
 
@@ -5179,8 +5573,23 @@
5179
5573
  return bestIndex;
5180
5574
  }
5181
5575
 
5576
+ function buildLiteralPreviewDisplayMap(text, rawOffsets) {
5577
+ const source = String(text || "");
5578
+ const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
5579
+ const charStarts = [];
5580
+ const charEnds = [];
5581
+ for (let i = 0; i < source.length; i += 1) {
5582
+ charStarts.push(rawMap[i]);
5583
+ charEnds.push(rawMap[i] + 1);
5584
+ }
5585
+ return buildNormalizedPreviewDisplayMap(source, charStarts, charEnds);
5586
+ }
5587
+
5182
5588
  function buildPreviewSelectionDisplayMap(blockText, kind) {
5183
5589
  const body = buildPreviewSelectionSourceBody(blockText, kind);
5590
+ if (kind === "code-line" || kind === "diff-line" || kind === "text-line") {
5591
+ return buildLiteralPreviewDisplayMap(body.text, body.rawOffsets);
5592
+ }
5184
5593
  const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
5185
5594
  return buildNormalizedPreviewDisplayMap(inlineMap.text, inlineMap.charStarts, inlineMap.charEnds);
5186
5595
  }
@@ -5197,6 +5606,7 @@
5197
5606
  function getPreviewCommentSelectionKey(selection) {
5198
5607
  if (!selection) return "";
5199
5608
  return [
5609
+ String(selection.paneId || ""),
5200
5610
  String(selection.blockKey || ""),
5201
5611
  String(selection.selectionStart || 0),
5202
5612
  String(selection.selectionEnd || 0),
@@ -5224,6 +5634,96 @@
5224
5634
  : null;
5225
5635
  }
5226
5636
 
5637
+ function getPreviewSelectionPaneIdForNode(node) {
5638
+ if (!node) return "";
5639
+ const element = node instanceof Element ? node : node.parentElement;
5640
+ const paneEl = element && typeof element.closest === "function"
5641
+ ? element.closest("#sourcePreview, #critiqueView")
5642
+ : null;
5643
+ return paneEl && paneEl.id ? String(paneEl.id) : "";
5644
+ }
5645
+
5646
+ function getPreviewSelectionPaneElement(paneId) {
5647
+ if (paneId === "sourcePreview") return sourcePreviewEl;
5648
+ if (paneId === "critiqueView") return critiqueViewEl;
5649
+ return null;
5650
+ }
5651
+
5652
+ function getActivePreviewSelectionForPane(paneId) {
5653
+ if (!paneId) return null;
5654
+ return activePreviewCommentSelection && activePreviewCommentSelection.paneId === paneId
5655
+ ? activePreviewCommentSelection
5656
+ : null;
5657
+ }
5658
+
5659
+ function ensurePreviewSelectionActions(targetEl) {
5660
+ if (!targetEl || typeof document.createElement !== "function") return null;
5661
+ const paneId = targetEl.id ? String(targetEl.id) : "";
5662
+ if (!paneId) return null;
5663
+ const existing = Array.from(targetEl.children || []).find((child) => child.classList && child.classList.contains("preview-selection-actions"));
5664
+ if (existing) {
5665
+ existing.dataset.previewPane = paneId;
5666
+ return existing;
5667
+ }
5668
+
5669
+ const actionsEl = document.createElement("div");
5670
+ actionsEl.className = "preview-selection-actions";
5671
+ actionsEl.dataset.previewPane = paneId;
5672
+ actionsEl.hidden = true;
5673
+
5674
+ const commentBtn = document.createElement("button");
5675
+ commentBtn.type = "button";
5676
+ commentBtn.className = "preview-comment-add";
5677
+ commentBtn.dataset.previewCommentAction = "comment";
5678
+ commentBtn.textContent = "Comment";
5679
+ commentBtn.hidden = true;
5680
+ actionsEl.appendChild(commentBtn);
5681
+
5682
+ const jumpBtn = document.createElement("button");
5683
+ jumpBtn.type = "button";
5684
+ jumpBtn.className = "preview-comment-jump";
5685
+ jumpBtn.dataset.previewCommentAction = "jump";
5686
+ jumpBtn.textContent = "Jump";
5687
+ jumpBtn.hidden = true;
5688
+ actionsEl.appendChild(jumpBtn);
5689
+
5690
+ targetEl.insertBefore(actionsEl, targetEl.firstChild || null);
5691
+ return actionsEl;
5692
+ }
5693
+
5694
+ function updatePreviewSelectionActions(targetEl) {
5695
+ if (!targetEl) return;
5696
+ const actionsEl = ensurePreviewSelectionActions(targetEl);
5697
+ if (!actionsEl) return;
5698
+ const paneId = targetEl.id ? String(targetEl.id) : "";
5699
+ const selection = getActivePreviewSelectionForPane(paneId);
5700
+ const commentBtn = actionsEl.querySelector(".preview-comment-add");
5701
+ const jumpBtn = actionsEl.querySelector(".preview-comment-jump");
5702
+ if (!selection) {
5703
+ actionsEl.hidden = true;
5704
+ if (commentBtn) commentBtn.hidden = true;
5705
+ if (jumpBtn) jumpBtn.hidden = true;
5706
+ return;
5707
+ }
5708
+ const lineLabel = summarizeReviewNoteAnchor(selection).toLowerCase();
5709
+ const blockKindLabel = getPreviewCommentBlockKindLabel(selection.previewCommentKind || "paragraph");
5710
+ actionsEl.hidden = false;
5711
+ if (commentBtn) {
5712
+ commentBtn.hidden = false;
5713
+ commentBtn.dataset.previewCommentMode = "selection";
5714
+ commentBtn.dataset.previewPane = paneId;
5715
+ commentBtn.title = "Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").";
5716
+ commentBtn.setAttribute("aria-label", commentBtn.title || "Comment");
5717
+ }
5718
+ if (jumpBtn) {
5719
+ jumpBtn.hidden = false;
5720
+ jumpBtn.dataset.previewCommentMode = "selection";
5721
+ jumpBtn.dataset.previewPane = paneId;
5722
+ jumpBtn.title = "Jump to the current preview selection on this " + blockKindLabel + " in the raw editor (" + lineLabel + ").";
5723
+ jumpBtn.setAttribute("aria-label", jumpBtn.title || "Jump");
5724
+ }
5725
+ }
5726
+
5227
5727
  function unwrapPreviewJumpHighlightElement(element) {
5228
5728
  if (!element || !element.parentNode) return;
5229
5729
  const parent = element.parentNode;
@@ -5277,8 +5777,9 @@
5277
5777
  }
5278
5778
 
5279
5779
  function scanSourcePreviewCommentBlocks(markdown) {
5280
- if (editorLanguage !== "markdown") return [];
5281
- return scanMarkdownPreviewCommentBlocks(markdown);
5780
+ if (editorLanguage === "markdown") return scanMarkdownPreviewCommentBlocks(markdown);
5781
+ if (editorLanguage === "latex") return scanLatexPreviewCommentBlocks(markdown);
5782
+ return [];
5282
5783
  }
5283
5784
 
5284
5785
  function scanMarkdownPreviewCommentBlocks(markdown) {
@@ -5497,8 +5998,199 @@
5497
5998
  return expandSourcePreviewCommentBlocksByDisplayMath(source, blocks);
5498
5999
  }
5499
6000
 
6001
+ function scanLatexPreviewCommentBlocks(markdown) {
6002
+ const source = String(markdown || "").replace(/\r\n/g, "\n");
6003
+ if (!source) return [];
6004
+ const bodyRange = findLatexDocumentBodyRange(source);
6005
+ const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
6006
+ const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
6007
+ const bodyText = source.slice(bodyStart, bodyEnd);
6008
+ const lines = bodyText.split("\n");
6009
+ const lineOffsets = [];
6010
+ let runningOffset = 0;
6011
+ for (const line of lines) {
6012
+ lineOffsets.push(runningOffset);
6013
+ runningOffset += line.length + 1;
6014
+ }
6015
+
6016
+ function getLine(index) {
6017
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
6018
+ }
6019
+
6020
+ function getStrippedLine(index) {
6021
+ return stripLatexPreviewComments(getLine(index)).trim();
6022
+ }
6023
+
6024
+ function isBlankLine(index) {
6025
+ return !getStrippedLine(index);
6026
+ }
6027
+
6028
+ function isBibliographyCommandLine(index) {
6029
+ return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
6030
+ }
6031
+
6032
+ function makeBlock(kind, startLineIndex, endLineIndex) {
6033
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
6034
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
6035
+ const start = bodyStart + (lineOffsets[safeStartLine] || 0);
6036
+ const end = bodyStart + (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
6037
+ return {
6038
+ kind,
6039
+ start,
6040
+ end,
6041
+ lineStart: getLineNumberAtOffset(source, start),
6042
+ lineEnd: getLineNumberAtOffset(source, Math.max(start, end - 1)),
6043
+ };
6044
+ }
6045
+
6046
+ function getChunkText(startLineIndex, endLineIndex) {
6047
+ return bodyText.slice(
6048
+ lineOffsets[startLineIndex] || 0,
6049
+ (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
6050
+ );
6051
+ }
6052
+
6053
+ function getEnvironmentStartName(index) {
6054
+ const line = getStrippedLine(index);
6055
+ const match = line.match(/^\\begin\{([^}]+)\}/);
6056
+ return match ? String(match[1] || "").trim().toLowerCase() : "";
6057
+ }
6058
+
6059
+ function findEnvironmentEndLine(startLineIndex, envName) {
6060
+ const openToken = "\\begin{" + envName + "}";
6061
+ const closeToken = "\\end{" + envName + "}";
6062
+ let depth = 0;
6063
+ for (let lineIndex = startLineIndex; lineIndex < lines.length; lineIndex += 1) {
6064
+ const line = getStrippedLine(lineIndex);
6065
+ if (line.includes(openToken)) depth += 1;
6066
+ if (line.includes(closeToken)) {
6067
+ depth -= 1;
6068
+ if (depth <= 0) return lineIndex;
6069
+ }
6070
+ }
6071
+ return startLineIndex;
6072
+ }
6073
+
6074
+ function isHeadingLine(index) {
6075
+ return Boolean(readLatexHeadingChunk(getLine(index)));
6076
+ }
6077
+
6078
+ function findBibliographyCommandEndLine(startLineIndex) {
6079
+ let endLineIndex = startLineIndex;
6080
+ for (let lineIndex = startLineIndex + 1; lineIndex < lines.length; lineIndex += 1) {
6081
+ if (!isBibliographyCommandLine(lineIndex)) break;
6082
+ endLineIndex = lineIndex;
6083
+ }
6084
+ return endLineIndex;
6085
+ }
6086
+
6087
+ function isMathStartLine(index) {
6088
+ const line = getStrippedLine(index);
6089
+ if (!line) return false;
6090
+ if (line.startsWith("$$") || line.startsWith("\\[")) return true;
6091
+ const envName = getEnvironmentStartName(index);
6092
+ return Boolean(envName && DISPLAY_MATH_ENV_NAMES.has(envName));
6093
+ }
6094
+
6095
+ function findMathEndLine(startLineIndex) {
6096
+ for (let endLineIndex = startLineIndex; endLineIndex < lines.length; endLineIndex += 1) {
6097
+ const chunkText = getChunkText(startLineIndex, endLineIndex);
6098
+ if (getStandaloneDisplayMathRange(stripLatexPreviewComments(chunkText))) {
6099
+ return endLineIndex;
6100
+ }
6101
+ }
6102
+ return startLineIndex;
6103
+ }
6104
+
6105
+ const blocks = [];
6106
+ let lineIndex = 0;
6107
+ while (lineIndex < lines.length) {
6108
+ if (isBlankLine(lineIndex)) {
6109
+ lineIndex += 1;
6110
+ continue;
6111
+ }
6112
+
6113
+ const strippedLine = getStrippedLine(lineIndex);
6114
+ const envName = getEnvironmentStartName(lineIndex);
6115
+
6116
+ if (isHeadingLine(lineIndex)) {
6117
+ blocks.push(makeBlock("heading", lineIndex, lineIndex));
6118
+ lineIndex += 1;
6119
+ continue;
6120
+ }
6121
+
6122
+ if (envName === "abstract" || envName === "keywords") {
6123
+ const endLineIndex = findEnvironmentEndLine(lineIndex, envName);
6124
+ const chunkText = getChunkText(lineIndex, endLineIndex);
6125
+ if (normalizeLatexPreviewBlockText(chunkText, "paragraph")) {
6126
+ blocks.push(makeBlock("paragraph", lineIndex, endLineIndex));
6127
+ }
6128
+ lineIndex = endLineIndex + 1;
6129
+ continue;
6130
+ }
6131
+
6132
+ if (envName && LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME.has(envName)) {
6133
+ const endLineIndex = findEnvironmentEndLine(lineIndex, envName);
6134
+ blocks.push(makeBlock(LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME.get(envName) || "paragraph", lineIndex, endLineIndex));
6135
+ lineIndex = endLineIndex + 1;
6136
+ continue;
6137
+ }
6138
+
6139
+ if (isBibliographyCommandLine(lineIndex)) {
6140
+ const endLineIndex = findBibliographyCommandEndLine(lineIndex);
6141
+ blocks.push(makeBlock("heading", lineIndex, endLineIndex));
6142
+ blocks.push(makeBlock("paragraph", lineIndex, endLineIndex));
6143
+ lineIndex = endLineIndex + 1;
6144
+ continue;
6145
+ }
6146
+
6147
+ if (envName && LATEX_PREVIEW_SKIPPED_ENV_NAMES.has(envName) && !DISPLAY_MATH_ENV_NAMES.has(envName)) {
6148
+ lineIndex = findEnvironmentEndLine(lineIndex, envName) + 1;
6149
+ continue;
6150
+ }
6151
+
6152
+ if (isMathStartLine(lineIndex)) {
6153
+ const endLineIndex = findMathEndLine(lineIndex);
6154
+ blocks.push(makeBlock("math", lineIndex, endLineIndex));
6155
+ lineIndex = endLineIndex + 1;
6156
+ continue;
6157
+ }
6158
+
6159
+ if (isLatexPreviewSkippableChunk(strippedLine)) {
6160
+ lineIndex += 1;
6161
+ continue;
6162
+ }
6163
+
6164
+ const paragraphStartLine = lineIndex;
6165
+ let paragraphEndLine = lineIndex;
6166
+ for (let nextLineIndex = lineIndex + 1; nextLineIndex < lines.length; nextLineIndex += 1) {
6167
+ if (isBlankLine(nextLineIndex) || isHeadingLine(nextLineIndex) || isMathStartLine(nextLineIndex)) {
6168
+ break;
6169
+ }
6170
+ const nextEnvName = getEnvironmentStartName(nextLineIndex);
6171
+ if (nextEnvName) {
6172
+ break;
6173
+ }
6174
+ paragraphEndLine = nextLineIndex;
6175
+ }
6176
+
6177
+ const chunkText = getChunkText(paragraphStartLine, paragraphEndLine);
6178
+ if (normalizeLatexPreviewBlockText(chunkText, "paragraph") && !isLatexPreviewSkippableChunk(chunkText)) {
6179
+ blocks.push(makeBlock("paragraph", paragraphStartLine, paragraphEndLine));
6180
+ }
6181
+ lineIndex = paragraphEndLine + 1;
6182
+ }
6183
+
6184
+ return blocks;
6185
+ }
6186
+
5500
6187
  function isPreviewDisplayMathElement(element) {
5501
- return Boolean(element && element instanceof Element && element.matches && element.matches("math[display='block'], .studio-mathjax-fallback-display"));
6188
+ return Boolean(
6189
+ element
6190
+ && element instanceof Element
6191
+ && element.matches
6192
+ && element.matches("math[display='block'], .studio-mathjax-fallback-display, .studio-display-equation, .studio-display-equation-body")
6193
+ );
5502
6194
  }
5503
6195
 
5504
6196
  function previewNodesHaveVisibleContent(nodes) {
@@ -5511,8 +6203,63 @@
5511
6203
  });
5512
6204
  }
5513
6205
 
6206
+ function wrapLoosePreviewInlineRunsAsParagraphs(targetEl) {
6207
+ if (!targetEl || !targetEl.childNodes || typeof document.createElement !== "function") return;
6208
+ const childNodes = Array.from(targetEl.childNodes || []);
6209
+ if (childNodes.length === 0) return;
6210
+
6211
+ function isDirectBlockChild(node) {
6212
+ if (!(node instanceof Element) || node.parentElement !== targetEl) return false;
6213
+ const tag = node.tagName ? node.tagName.toUpperCase() : "";
6214
+ if (/^H[1-6]$/.test(tag)) return true;
6215
+ if (tag === "P" || tag === "BLOCKQUOTE" || tag === "UL" || tag === "OL" || tag === "TABLE" || tag === "PRE" || tag === "HEADER" || tag === "FIGURE") {
6216
+ return true;
6217
+ }
6218
+ if (tag === "MATH") {
6219
+ return String(node.getAttribute("display") || "").toLowerCase() === "block";
6220
+ }
6221
+ if (tag === "DIV") return true;
6222
+ return false;
6223
+ }
6224
+
6225
+ let runNodes = [];
6226
+
6227
+ function flushRun(referenceNode) {
6228
+ if (runNodes.length === 0) return;
6229
+ if (!previewNodesHaveVisibleContent(runNodes)) {
6230
+ runNodes.forEach((node) => {
6231
+ if (node && node.parentNode === targetEl) {
6232
+ targetEl.removeChild(node);
6233
+ }
6234
+ });
6235
+ runNodes = [];
6236
+ return;
6237
+ }
6238
+ const paragraphEl = document.createElement("p");
6239
+ runNodes.forEach((node) => {
6240
+ paragraphEl.appendChild(node);
6241
+ });
6242
+ targetEl.insertBefore(paragraphEl, referenceNode || null);
6243
+ runNodes = [];
6244
+ }
6245
+
6246
+ childNodes.forEach((node) => {
6247
+ if (node instanceof Element && isDirectBlockChild(node)) {
6248
+ flushRun(node);
6249
+ return;
6250
+ }
6251
+ if (node.parentNode === targetEl) {
6252
+ runNodes.push(node);
6253
+ }
6254
+ });
6255
+ flushRun(null);
6256
+ }
6257
+
5514
6258
  function splitMixedPreviewParagraphsAroundDisplayMath(targetEl) {
5515
6259
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6260
+ if (editorLanguage === "latex") {
6261
+ wrapLoosePreviewInlineRunsAsParagraphs(targetEl);
6262
+ }
5516
6263
  Array.from(targetEl.querySelectorAll("p")).forEach((paragraphEl) => {
5517
6264
  if (!(paragraphEl instanceof Element) || !paragraphEl.parentNode) return;
5518
6265
  if (paragraphEl.closest && paragraphEl.closest(".preview-comment-block")) return;
@@ -5574,6 +6321,20 @@
5574
6321
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
5575
6322
  if (/^H[1-6]$/.test(tag)) return "heading";
5576
6323
  if (tag === "P") return "paragraph";
6324
+ if (tag === "FIGURE") {
6325
+ if (element.classList && element.classList.contains("studio-algorithm-block")) {
6326
+ return "algorithm";
6327
+ }
6328
+ return editorLanguage === "latex" ? "figure" : "";
6329
+ }
6330
+ if (tag === "DIV" && element.classList) {
6331
+ if (element.classList.contains("studio-display-equation")) {
6332
+ return "math";
6333
+ }
6334
+ if (element.classList.contains("abstract") || element.classList.contains("keywords") || element.classList.contains("references")) {
6335
+ return "paragraph";
6336
+ }
6337
+ }
5577
6338
  if (tag === "BLOCKQUOTE") return "blockquote";
5578
6339
  if (tag === "UL" || tag === "OL") return "list";
5579
6340
  if (tag === "TABLE") return "table";
@@ -5605,11 +6366,49 @@
5605
6366
  return Boolean(getPreviewCommentTargetKind(element));
5606
6367
  }
5607
6368
 
6369
+ function isLatexPreviewCommentTargetElement(element, targetEl) {
6370
+ if (!element || !(element instanceof Element) || !targetEl) return false;
6371
+ const kind = getPreviewCommentTargetKind(element);
6372
+ if (kind === "heading" || kind === "paragraph" || kind === "figure" || kind === "algorithm" || kind === "table") {
6373
+ if (element.parentElement === targetEl) return true;
6374
+ if (
6375
+ kind === "paragraph"
6376
+ && element.classList
6377
+ && element.classList.contains("abstract")
6378
+ && element.parentElement
6379
+ && element.parentElement.tagName === "HEADER"
6380
+ && element.parentElement.id === "title-block-header"
6381
+ && element.parentElement.parentElement === targetEl
6382
+ ) {
6383
+ return true;
6384
+ }
6385
+ return false;
6386
+ }
6387
+ if (kind === "math") {
6388
+ if (element.parentElement === targetEl) return true;
6389
+ const bodyEl = element.parentElement;
6390
+ const frameEl = bodyEl && bodyEl.parentElement;
6391
+ return Boolean(
6392
+ bodyEl
6393
+ && bodyEl.classList
6394
+ && bodyEl.classList.contains("studio-display-equation-body")
6395
+ && frameEl
6396
+ && frameEl.classList
6397
+ && frameEl.classList.contains("studio-display-equation")
6398
+ && frameEl.parentElement === targetEl
6399
+ );
6400
+ }
6401
+ return false;
6402
+ }
6403
+
5608
6404
  function collectPreviewCommentTargetElements(targetEl) {
5609
6405
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
5610
- const selector = "h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, table, div.sourceCode, pre, math[display='block'], .studio-mathjax-fallback-display, .studio-page-break, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
6406
+ const selector = "h1, h2, h3, h4, h5, h6, p, figure, blockquote, ul, ol, table, div.sourceCode, pre, math[display='block'], .studio-display-equation, .studio-mathjax-fallback-display, .studio-page-break, .abstract, .keywords, .references, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
5611
6407
  return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
5612
6408
  if (!isPreviewCommentTargetElement(element)) return false;
6409
+ if (editorLanguage === "latex" && !isLatexPreviewCommentTargetElement(element, targetEl)) {
6410
+ return false;
6411
+ }
5613
6412
  let ancestor = element.parentElement;
5614
6413
  while (ancestor && ancestor !== targetEl) {
5615
6414
  if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
@@ -5626,6 +6425,9 @@
5626
6425
  function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
5627
6426
  if (!sourceBlock) return "";
5628
6427
  const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
6428
+ if (editorLanguage === "latex") {
6429
+ return normalizeLatexPreviewBlockText(blockText, sourceBlock.kind);
6430
+ }
5629
6431
  if (sourceBlock.kind === "page-break") {
5630
6432
  const match = blockText.trim().match(/^\\(newpage|pagebreak|clearpage)/i);
5631
6433
  return match ? String(match[1] || "").toLowerCase() : "page-break";
@@ -5673,13 +6475,46 @@
5673
6475
  return longer.includes(shorter);
5674
6476
  }
5675
6477
 
6478
+ function tokenizePreviewComparableText(text) {
6479
+ return normalizeVisiblePreviewText(text)
6480
+ .toLowerCase()
6481
+ .split(/\s+/)
6482
+ .map((token) => token.replace(/^[^0-9A-Za-z\u00C0-\uFFFF]+|[^0-9A-Za-z\u00C0-\uFFFF]+$/g, ""))
6483
+ .filter((token) => token && (token.length >= 4 || /[A-Za-z\u00C0-\uFFFF]/.test(token)));
6484
+ }
6485
+
6486
+ function getHighConfidenceLatexOrderedTokenMatchScore(targetText, desiredText) {
6487
+ if (editorLanguage !== "latex") return -1;
6488
+ const targetTokens = tokenizePreviewComparableText(targetText);
6489
+ const desiredTokens = tokenizePreviewComparableText(desiredText);
6490
+ if (targetTokens.length === 0 || desiredTokens.length < 5) return -1;
6491
+
6492
+ let targetTokenIndex = 0;
6493
+ let matchedCount = 0;
6494
+ for (const token of desiredTokens) {
6495
+ while (targetTokenIndex < targetTokens.length && targetTokens[targetTokenIndex] !== token) {
6496
+ targetTokenIndex += 1;
6497
+ }
6498
+ if (targetTokenIndex >= targetTokens.length) break;
6499
+ matchedCount += 1;
6500
+ targetTokenIndex += 1;
6501
+ }
6502
+
6503
+ const matchRatio = matchedCount / desiredTokens.length;
6504
+ if (matchedCount < 5 || matchRatio < 0.6) return -1;
6505
+ return matchedCount * 1000 + Math.round(matchRatio * 100);
6506
+ }
6507
+
5676
6508
  function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5677
6509
  const desiredKind = sourceBlock ? sourceBlock.kind : "";
5678
6510
  const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
6511
+ const preferredStartIndex = Math.max(0, startIndex || 0);
5679
6512
  let fallbackIndex = -1;
5680
6513
  let containsIndex = -1;
6514
+ let orderedTokenIndex = -1;
6515
+ let orderedTokenScore = Number.NEGATIVE_INFINITY;
5681
6516
 
5682
- for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
6517
+ for (let i = preferredStartIndex; i < targetBlocks.length; i += 1) {
5683
6518
  const targetEntry = targetBlocks[i];
5684
6519
  if (!targetEntry || targetEntry.kind !== desiredKind) continue;
5685
6520
  if (fallbackIndex < 0) fallbackIndex = i;
@@ -5691,10 +6526,19 @@
5691
6526
  if (containsIndex < 0 && isHighConfidencePreviewTextContainmentMatch(targetText, desiredText)) {
5692
6527
  containsIndex = i;
5693
6528
  }
6529
+ const latexTokenScore = getHighConfidenceLatexOrderedTokenMatchScore(targetText, desiredText);
6530
+ if (latexTokenScore >= 0) {
6531
+ const score = latexTokenScore - (Math.abs(i - preferredStartIndex) * 4);
6532
+ if (score > orderedTokenScore) {
6533
+ orderedTokenScore = score;
6534
+ orderedTokenIndex = i;
6535
+ }
6536
+ }
5694
6537
  }
5695
6538
  }
5696
6539
 
5697
6540
  if (containsIndex >= 0) return containsIndex;
6541
+ if (orderedTokenIndex >= 0) return orderedTokenIndex;
5698
6542
  return fallbackIndex;
5699
6543
  }
5700
6544
 
@@ -5709,41 +6553,26 @@
5709
6553
 
5710
6554
  function updatePreviewCommentBlockState(blockEl, sourceText, displayNotes) {
5711
6555
  if (!blockEl || !blockEl.dataset) return;
5712
- const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5713
- const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5714
- const summaryBtn = blockEl.querySelector(".preview-comment-summary");
5715
- const addBtn = blockEl.querySelector(".preview-comment-add");
5716
- const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
5717
- const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
5718
6556
  const blockKey = getPreviewCommentBlockKey(blockEl);
5719
- const hasSelection = Boolean(activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey);
6557
+ const paneId = getPreviewSelectionPaneIdForNode(blockEl);
6558
+ const hasSelection = Boolean(
6559
+ activePreviewCommentSelection
6560
+ && activePreviewCommentSelection.paneId === paneId
6561
+ && activePreviewCommentSelection.blockKey === blockKey
6562
+ );
5720
6563
 
5721
6564
  blockEl.classList.remove("has-comments");
5722
6565
  blockEl.classList.toggle("has-selection", hasSelection);
5723
-
5724
- if (summaryBtn) {
5725
- summaryBtn.hidden = true;
5726
- summaryBtn.textContent = "";
5727
- summaryBtn.dataset.reviewNoteId = "";
5728
- }
5729
-
5730
- if (addBtn) {
5731
- addBtn.hidden = !hasSelection;
5732
- addBtn.textContent = "Comment";
5733
- addBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
5734
- addBtn.title = hasSelection
5735
- ? ("Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").")
5736
- : "";
5737
- addBtn.setAttribute("aria-label", addBtn.title || "Comment");
5738
- }
5739
6566
  }
5740
6567
 
5741
6568
  function updatePreviewCommentBlocksForElement(targetEl) {
5742
6569
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6570
+ ensurePreviewSelectionActions(targetEl);
5743
6571
  const sourceText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5744
6572
  Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5745
6573
  updatePreviewCommentBlockState(blockEl, sourceText);
5746
6574
  });
6575
+ updatePreviewSelectionActions(targetEl);
5747
6576
  }
5748
6577
 
5749
6578
  function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
@@ -5771,35 +6600,20 @@
5771
6600
  wrapper.dataset.reviewNoteLineEnd = String(sourceBlock.lineEnd);
5772
6601
  wrapper.dataset.previewCommentKind = sourceBlock.kind;
5773
6602
 
5774
- const controls = document.createElement("div");
5775
- controls.className = "preview-comment-controls";
5776
-
5777
- const summaryBtn = document.createElement("button");
5778
- summaryBtn.type = "button";
5779
- summaryBtn.className = "preview-comment-summary";
5780
- summaryBtn.hidden = true;
5781
- controls.appendChild(summaryBtn);
5782
-
5783
- const addBtn = document.createElement("button");
5784
- addBtn.type = "button";
5785
- addBtn.className = "preview-comment-add";
5786
- addBtn.textContent = "Comment";
5787
- controls.appendChild(addBtn);
5788
-
5789
6603
  originalElement.replaceWith(wrapper);
5790
- wrapper.appendChild(controls);
5791
6604
  originalElement.classList.add("preview-comment-block-content");
5792
6605
  wrapper.appendChild(originalElement);
5793
6606
  }
5794
6607
 
6608
+ ensurePreviewSelectionActions(targetEl);
5795
6609
  updatePreviewCommentBlocksForElement(targetEl);
5796
6610
  }
5797
6611
 
5798
6612
  function refreshRenderedEditorPreviewComments() {
5799
- if (sourcePreviewEl && !sourcePreviewEl.hidden) {
6613
+ if (sourcePreviewEl) {
5800
6614
  updatePreviewCommentBlocksForElement(sourcePreviewEl);
5801
6615
  }
5802
- if (critiqueViewEl && rightView === "editor-preview") {
6616
+ if (critiqueViewEl) {
5803
6617
  updatePreviewCommentBlocksForElement(critiqueViewEl);
5804
6618
  }
5805
6619
  }
@@ -5845,6 +6659,19 @@
5845
6659
  };
5846
6660
  }
5847
6661
 
6662
+ if (editorLanguage === "latex") {
6663
+ const selectedDisplayText = buildNormalizedPreviewRangeText(range);
6664
+ if (!selectedDisplayText) return null;
6665
+ return {
6666
+ selectionStart: blockStart,
6667
+ selectionEnd: blockEnd,
6668
+ lineStart: getLineNumberAtOffset(source, blockStart),
6669
+ lineEnd: getLineNumberAtOffset(source, Math.max(blockStart, blockEnd - 1)),
6670
+ selectedText: source.slice(blockStart, blockEnd),
6671
+ selectedDisplayText,
6672
+ };
6673
+ }
6674
+
5848
6675
  const sourceBlockText = source.slice(blockStart, blockEnd);
5849
6676
  const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5850
6677
  if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
@@ -5992,7 +6819,16 @@
5992
6819
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5993
6820
  const range = resolveReviewNoteRange(note, source);
5994
6821
  if (!range) return false;
5995
- const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
6822
+ const rangeBlock = findPreviewCommentBlockForRange(targetEl, range);
6823
+ const selectionText = getPreviewNoteNormalizedSelectionText(note);
6824
+ let blockEl = rangeBlock;
6825
+ if (selectionText) {
6826
+ const rangeContentEl = rangeBlock ? (rangeBlock.querySelector(".preview-comment-block-content") || rangeBlock) : null;
6827
+ const rangeText = rangeContentEl ? buildNormalizedPreviewSearchText(rangeContentEl) : "";
6828
+ if (!rangeText || !rangeText.includes(selectionText)) {
6829
+ blockEl = findPreviewCommentBlockForNoteText(targetEl, note) || rangeBlock;
6830
+ }
6831
+ }
5996
6832
  if (!blockEl) return false;
5997
6833
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5998
6834
  if (String(blockEl.dataset && blockEl.dataset.previewCommentKind || "") === "math") {
@@ -6011,10 +6847,11 @@
6011
6847
  }
6012
6848
 
6013
6849
  function revealReviewNoteInPreview(note) {
6014
- if (!supportsPreviewCommentsForCurrentEditor()) return;
6850
+ if (!supportsPreviewCommentsForCurrentEditor()) return false;
6015
6851
  if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
6016
- revealReviewNoteInPreviewElement(critiqueViewEl, note);
6852
+ return revealReviewNoteInPreviewElement(critiqueViewEl, note);
6017
6853
  }
6854
+ return false;
6018
6855
  }
6019
6856
 
6020
6857
  function updateActivePreviewCommentSelectionFromDom() {
@@ -6046,7 +6883,9 @@
6046
6883
 
6047
6884
  setActivePreviewCommentSelection({
6048
6885
  ...anchor,
6886
+ paneId: getPreviewSelectionPaneIdForNode(startBlock),
6049
6887
  blockKey: getPreviewCommentBlockKey(startBlock),
6888
+ previewCommentKind: String(startBlock.dataset && startBlock.dataset.previewCommentKind || "paragraph"),
6050
6889
  });
6051
6890
  }
6052
6891
 
@@ -6141,11 +6980,27 @@
6141
6980
  && typeof sourceTextEl.selectionEnd === "number"
6142
6981
  && sourceTextEl.selectionEnd > sourceTextEl.selectionStart
6143
6982
  );
6983
+ const canJumpToPreview = Boolean(
6984
+ hasSelection
6985
+ && rightView === "editor-preview"
6986
+ && critiqueViewEl
6987
+ && supportsPreviewCommentsForCurrentEditor()
6988
+ );
6144
6989
  editorSelectionCommentBtn.hidden = !hasSelection;
6990
+ if (editorSelectionJumpBtn) {
6991
+ editorSelectionJumpBtn.hidden = !canJumpToPreview;
6992
+ }
6993
+ if (editorSelectionActionsEl) {
6994
+ editorSelectionActionsEl.hidden = !hasSelection;
6995
+ }
6145
6996
  if (hasSelection) {
6146
6997
  editorSelectionCommentBtn.title = "Create a new local comment from the current editor selection.";
6147
6998
  editorSelectionCommentBtn.setAttribute("aria-label", editorSelectionCommentBtn.title);
6148
6999
  }
7000
+ if (editorSelectionJumpBtn && canJumpToPreview) {
7001
+ editorSelectionJumpBtn.title = "Jump to the current editor selection in the preview.";
7002
+ editorSelectionJumpBtn.setAttribute("aria-label", editorSelectionJumpBtn.title);
7003
+ }
6149
7004
  }
6150
7005
 
6151
7006
  function clearSuppressedEditorSelectionComment() {
@@ -6362,12 +7217,12 @@
6362
7217
  });
6363
7218
  }
6364
7219
 
6365
- function addReviewNoteFromPreviewSelection(blockEl) {
6366
- if (!blockEl) return null;
6367
- const blockKey = getPreviewCommentBlockKey(blockEl);
6368
- const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
6369
- ? activePreviewCommentSelection
6370
- : null;
7220
+ function getActivePreviewSelectionAnchorForPane(paneId) {
7221
+ return getActivePreviewSelectionForPane(paneId);
7222
+ }
7223
+
7224
+ function addReviewNoteFromPreviewSelection(paneId) {
7225
+ const anchor = getActivePreviewSelectionAnchorForPane(paneId);
6371
7226
  if (!anchor) {
6372
7227
  setStatus("Select some preview text within a single block first.", "warning");
6373
7228
  return null;
@@ -6403,6 +7258,12 @@
6403
7258
  if (editorSelectionCommentBtn) {
6404
7259
  editorSelectionCommentBtn.hidden = true;
6405
7260
  }
7261
+ if (editorSelectionJumpBtn) {
7262
+ editorSelectionJumpBtn.hidden = true;
7263
+ }
7264
+ if (editorSelectionActionsEl) {
7265
+ editorSelectionActionsEl.hidden = true;
7266
+ }
6406
7267
  const shouldOpenReviewNotes = !isReviewNotesOpen();
6407
7268
  pendingReviewNoteFocusId = note.id;
6408
7269
  setReviewNotes(reviewNotes.concat([note]));
@@ -6432,6 +7293,30 @@
6432
7293
  });
6433
7294
  }
6434
7295
 
7296
+ function jumpToEditorSelectionInPreview() {
7297
+ if (editorView !== "markdown") {
7298
+ setStatus("Switch to Editor (Raw) before jumping from an editor selection.", "warning");
7299
+ return false;
7300
+ }
7301
+ if (rightView !== "editor-preview" || !critiqueViewEl || !supportsPreviewCommentsForCurrentEditor()) {
7302
+ setStatus("Open Editor (Preview) on the right to jump the current editor selection there.", "warning");
7303
+ return false;
7304
+ }
7305
+ const anchor = getEditorAnchorForReviewNote();
7306
+ const jumped = revealReviewNoteInPreview(anchor);
7307
+ if (!jumped) {
7308
+ setStatus("Could not find the current editor selection in the preview.", "warning");
7309
+ return false;
7310
+ }
7311
+ const current = String(sourceTextEl.value || "");
7312
+ const range = resolveReviewNoteRange(anchor, current);
7313
+ if (range) {
7314
+ scrollEditorRangeIntoView(range);
7315
+ }
7316
+ setStatus("Jumped to the current editor selection in the preview.", "success");
7317
+ return true;
7318
+ }
7319
+
6435
7320
  function addReviewNoteFromEditorLine() {
6436
7321
  if (editorView !== "markdown") {
6437
7322
  setStatus("Switch to Editor (Raw) before adding a line comment.", "warning");
@@ -6442,14 +7327,13 @@
6442
7327
  });
6443
7328
  }
6444
7329
 
6445
- function jumpToReviewNote(noteId) {
6446
- const note = reviewNotes.find((entry) => entry && entry.id === noteId);
6447
- if (!note) return;
7330
+ function jumpToReviewAnchor(anchor, options) {
7331
+ if (!anchor) return false;
6448
7332
  const current = String(sourceTextEl.value || "");
6449
- const range = resolveReviewNoteRange(note, current);
7333
+ const range = resolveReviewNoteRange(anchor, current);
6450
7334
  if (!range) {
6451
- setStatus("Could not find the anchored location for this comment.", "warning");
6452
- return;
7335
+ setStatus((options && options.notFoundStatusMessage) || "Could not find the anchored location.", "warning");
7336
+ return false;
6453
7337
  }
6454
7338
  suppressEditorSelectionComment = true;
6455
7339
  suppressedEditorSelectionStart = range.start;
@@ -6464,9 +7348,56 @@
6464
7348
  : (cb) => window.setTimeout(cb, 16);
6465
7349
  schedule(() => {
6466
7350
  scrollEditorRangeIntoView(range);
6467
- revealReviewNoteInPreview(note);
7351
+ if (options && typeof options.afterJump === "function") {
7352
+ options.afterJump(range);
7353
+ }
6468
7354
  updateEditorSelectionCommentUi();
6469
7355
  });
7356
+ if (!options || options.status !== false) {
7357
+ setStatus((options && options.statusMessage) || "Jumped to anchored location in the editor.", "success");
7358
+ }
7359
+ return true;
7360
+ }
7361
+
7362
+ function jumpToPreviewSelection(paneId) {
7363
+ const anchor = getActivePreviewSelectionAnchorForPane(paneId);
7364
+ if (!anchor) {
7365
+ setStatus("Select some preview text within a single block first.", "warning");
7366
+ return false;
7367
+ }
7368
+ const previewNote = normalizeReviewNote(anchor);
7369
+ const jumped = jumpToReviewAnchor(previewNote, {
7370
+ statusMessage: "Jumped to preview selection in the raw editor.",
7371
+ afterJump: () => {
7372
+ const paneEl = getPreviewSelectionPaneElement(paneId);
7373
+ if (paneEl && previewNote) {
7374
+ revealReviewNoteInPreviewElement(paneEl, previewNote);
7375
+ }
7376
+ const schedule = typeof window.requestAnimationFrame === "function"
7377
+ ? window.requestAnimationFrame.bind(window)
7378
+ : (cb) => window.setTimeout(cb, 16);
7379
+ schedule(() => {
7380
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
7381
+ if (selection && typeof selection.removeAllRanges === "function") {
7382
+ selection.removeAllRanges();
7383
+ }
7384
+ clearPreviewCommentSelection();
7385
+ });
7386
+ },
7387
+ });
7388
+ return jumped;
7389
+ }
7390
+
7391
+ function jumpToReviewNote(noteId) {
7392
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
7393
+ if (!note) return;
7394
+ jumpToReviewAnchor(note, {
7395
+ status: false,
7396
+ notFoundStatusMessage: "Could not find the anchored location for this comment.",
7397
+ afterJump: () => {
7398
+ revealReviewNoteInPreview(note);
7399
+ },
7400
+ });
6470
7401
  }
6471
7402
 
6472
7403
  function deleteReviewNote(noteId) {
@@ -8380,6 +9311,15 @@
8380
9311
  });
8381
9312
  }
8382
9313
 
9314
+ if (editorSelectionJumpBtn) {
9315
+ editorSelectionJumpBtn.addEventListener("mousedown", (event) => {
9316
+ event.preventDefault();
9317
+ });
9318
+ editorSelectionJumpBtn.addEventListener("click", () => {
9319
+ jumpToEditorSelectionInPreview();
9320
+ });
9321
+ }
9322
+
8383
9323
  if (reviewNotesInlineAllBtn) {
8384
9324
  reviewNotesInlineAllBtn.addEventListener("click", () => {
8385
9325
  toggleAllReviewNotesInlineAnnotations();
@@ -8399,22 +9339,26 @@
8399
9339
 
8400
9340
  function handlePreviewCommentActionMouseDown(event) {
8401
9341
  const target = event.target;
8402
- const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
9342
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
8403
9343
  if (!actionBtn) return;
8404
9344
  event.preventDefault();
8405
9345
  }
8406
9346
 
8407
9347
  function handlePreviewCommentActionClick(event) {
8408
9348
  const target = event.target;
8409
- const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
9349
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
8410
9350
  if (!actionBtn) return;
8411
- const blockEl = actionBtn.closest(".preview-comment-block");
8412
- if (!blockEl) return;
8413
9351
  event.preventDefault();
8414
9352
  event.stopPropagation();
8415
9353
  const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
8416
9354
  if (!mode || !mode.startsWith("selection")) return;
8417
- addReviewNoteFromPreviewSelection(blockEl);
9355
+ const paneId = String(actionBtn.dataset && actionBtn.dataset.previewPane ? actionBtn.dataset.previewPane : "");
9356
+ const action = String(actionBtn.dataset && actionBtn.dataset.previewCommentAction ? actionBtn.dataset.previewCommentAction : "comment");
9357
+ if (action === "jump") {
9358
+ jumpToPreviewSelection(paneId);
9359
+ return;
9360
+ }
9361
+ addReviewNoteFromPreviewSelection(paneId);
8418
9362
  }
8419
9363
 
8420
9364
  if (leftPaneEl) {