pi-studio 0.5.49 → 0.5.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.50] — 2026-04-09
8
+
9
+ ### Added
10
+ - Editor-preview comments now also work for minimal structured LaTeX preview content, including headings, prose paragraphs, references sections, and whole display equations.
11
+ - Preview-side selection affordances now include a transient **Jump** action alongside **Comment**, so you can reveal the corresponding raw-editor span without creating a local comment first.
12
+
13
+ ### Fixed
14
+ - LaTeX preview comment mapping now follows Studio's rendered structure more closely, including title-block abstracts, loose prose after decorated display equations, bibliography/reference sections, and paragraph alignment around figures and rendered citation/reference text.
15
+ - Preview comment controls for display equations now anchor to the outer rendered equation frame, improving visibility and making preview jump targeting more reliable.
16
+
7
17
  ## [0.5.49] — 2026-04-09
8
18
 
9
19
  ### Fixed
package/README.md CHANGED
@@ -18,7 +18,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
18
18
  - Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces
19
19
  - Runs editor text directly, or asks for structured critique (auto/writing/code focus)
20
20
  - Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
21
- - Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with **Comment** actions from raw-editor selections plus editor-preview selections for Markdown and code/text/diff previews, alongside optional inline `[an: ...]` toggles when you want comments reflected in the document text
21
+ - Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with **Comment** actions from raw-editor selections plus editor-preview selections for Markdown, LaTeX, and code/text/diff previews, alongside a transient preview **Jump** action and optional inline `[an: ...]` toggles when you want comments reflected in the document text
22
22
  - Browses response history (`Prev/Next/Last`) and loads either:
23
23
  - response text
24
24
  - critique notes/full critique
@@ -1904,6 +1904,7 @@
1904
1904
 
1905
1905
  fallbackTargets.forEach((entry) => {
1906
1906
  entry.renderTarget.classList.add("studio-mathjax-fallback");
1907
+ entry.renderTarget.setAttribute("data-tex-source", entry.tex);
1907
1908
  if (entry.displayMode) {
1908
1909
  entry.renderTarget.classList.add("studio-mathjax-fallback-display");
1909
1910
  entry.renderTarget.textContent = "\\[\n" + entry.tex + "\n\\]";
@@ -3860,7 +3861,8 @@
3860
3861
  + ">"
3861
3862
  + "<div class='preview-comment-controls'>"
3862
3863
  + "<button type='button' class='preview-comment-summary' hidden></button>"
3863
- + "<button type='button' class='preview-comment-add'>Comment</button>"
3864
+ + "<button type='button' class='preview-comment-add' data-preview-comment-action='comment'>Comment</button>"
3865
+ + "<button type='button' class='preview-comment-jump' data-preview-comment-action='jump'>Jump</button>"
3864
3866
  + "</div>"
3865
3867
  + "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
3866
3868
  + "</div>",
@@ -4447,10 +4449,9 @@
4447
4449
  }
4448
4450
 
4449
4451
  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();
4452
+ return editorLanguage === "markdown"
4453
+ || editorLanguage === "latex"
4454
+ || supportsCodePreviewCommentsForCurrentEditor();
4454
4455
  }
4455
4456
 
4456
4457
  function getPreviewCommentBlockKindLabel(kind) {
@@ -4458,6 +4459,8 @@
4458
4459
  if (kind === "blockquote") return "quote block";
4459
4460
  if (kind === "list") return "list";
4460
4461
  if (kind === "math") return "equation";
4462
+ if (kind === "figure") return "figure";
4463
+ if (kind === "algorithm") return "algorithm block";
4461
4464
  if (kind === "page-break") return "page break";
4462
4465
  if (kind === "code") return "code block";
4463
4466
  if (kind === "table") return "table";
@@ -4670,6 +4673,389 @@
4670
4673
  bodyText: String(range.bodyText || ""),
4671
4674
  };
4672
4675
  }
4676
+
4677
+ const LATEX_PREVIEW_HEADING_COMMANDS = new Set([
4678
+ "part",
4679
+ "chapter",
4680
+ "section",
4681
+ "subsection",
4682
+ "subsubsection",
4683
+ "paragraph",
4684
+ "subparagraph",
4685
+ ]);
4686
+ const LATEX_PREVIEW_VISIBLE_GROUP_COMMANDS = new Set([
4687
+ "part",
4688
+ "chapter",
4689
+ "section",
4690
+ "subsection",
4691
+ "subsubsection",
4692
+ "paragraph",
4693
+ "subparagraph",
4694
+ "title",
4695
+ "author",
4696
+ "caption",
4697
+ "text",
4698
+ "textbf",
4699
+ "textit",
4700
+ "emph",
4701
+ "underline",
4702
+ "texttt",
4703
+ "textrm",
4704
+ "textsf",
4705
+ "textsc",
4706
+ "mbox",
4707
+ "makebox",
4708
+ "framebox",
4709
+ "fbox",
4710
+ "url",
4711
+ "path",
4712
+ "nolinkurl",
4713
+ ]);
4714
+ const LATEX_PREVIEW_SECOND_ARG_VISIBLE_COMMANDS = new Set([
4715
+ "href",
4716
+ "hyperref",
4717
+ ]);
4718
+ const LATEX_PREVIEW_HIDDEN_COMMANDS = new Set([
4719
+ "label",
4720
+ "ref",
4721
+ "eqref",
4722
+ "autoref",
4723
+ "pageref",
4724
+ "cite",
4725
+ "citet",
4726
+ "citep",
4727
+ "citealt",
4728
+ "citeauthor",
4729
+ "nocite",
4730
+ "footnote",
4731
+ "marginpar",
4732
+ "index",
4733
+ "includegraphics",
4734
+ "addbibresource",
4735
+ ]);
4736
+ const LATEX_PREVIEW_SKIPPED_ENV_NAMES = new Set([
4737
+ "document",
4738
+ "thebibliography",
4739
+ "itemize",
4740
+ "enumerate",
4741
+ "description",
4742
+ "figure",
4743
+ "figure*",
4744
+ "table",
4745
+ "table*",
4746
+ "tabular",
4747
+ "tabular*",
4748
+ "theorem",
4749
+ "lemma",
4750
+ "proposition",
4751
+ "corollary",
4752
+ "definition",
4753
+ "proof",
4754
+ "remark",
4755
+ "example",
4756
+ "verbatim",
4757
+ "lstlisting",
4758
+ "minted",
4759
+ "algorithm",
4760
+ "algorithm*",
4761
+ "algorithmic",
4762
+ ]);
4763
+ const LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME = new Map([
4764
+ ["figure", "figure"],
4765
+ ["figure*", "figure"],
4766
+ ["table", "table"],
4767
+ ["table*", "table"],
4768
+ ["algorithm", "algorithm"],
4769
+ ["algorithm*", "algorithm"],
4770
+ ]);
4771
+
4772
+ function stripLatexPreviewComments(text) {
4773
+ const source = String(text || "");
4774
+ let out = "";
4775
+ for (let index = 0; index < source.length; index += 1) {
4776
+ const ch = source[index];
4777
+ if (ch === "%" && !isEscapedAt(source, index)) {
4778
+ while (index < source.length && source[index] !== "\n") index += 1;
4779
+ if (index < source.length && source[index] === "\n") {
4780
+ out += "\n";
4781
+ }
4782
+ continue;
4783
+ }
4784
+ out += ch;
4785
+ }
4786
+ return out;
4787
+ }
4788
+
4789
+ function skipLatexPreviewCommentSpace(source, startIndex) {
4790
+ let index = Math.max(0, Number(startIndex) || 0);
4791
+ while (index < source.length) {
4792
+ const ch = source[index];
4793
+ if (/\s/.test(ch)) {
4794
+ index += 1;
4795
+ continue;
4796
+ }
4797
+ if (ch === "%" && !isEscapedAt(source, index)) {
4798
+ while (index < source.length && source[index] !== "\n") index += 1;
4799
+ continue;
4800
+ }
4801
+ break;
4802
+ }
4803
+ return index;
4804
+ }
4805
+
4806
+ function readLatexHeadingChunk(chunkText) {
4807
+ const source = String(chunkText || "");
4808
+ let index = skipLatexPreviewCommentSpace(source, 0);
4809
+ const command = parseLatexCommandAt(source, index);
4810
+ const commandName = command && command.name
4811
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
4812
+ : "";
4813
+ if (!command || !LATEX_PREVIEW_HEADING_COMMANDS.has(commandName)) return null;
4814
+ index = skipLatexPreviewCommentSpace(source, command.end);
4815
+ if (source[index] === "[") {
4816
+ const optionalGroup = readBalancedLatexGroup(source, index, "[", "]");
4817
+ if (optionalGroup) {
4818
+ index = skipLatexPreviewCommentSpace(source, optionalGroup.end);
4819
+ }
4820
+ }
4821
+ if (source[index] !== "{") return null;
4822
+ const titleGroup = readBalancedLatexGroup(source, index, "{", "}");
4823
+ if (!titleGroup) return null;
4824
+ index = skipLatexPreviewCommentSpace(source, titleGroup.end);
4825
+ while (index < source.length) {
4826
+ const trailingCommand = parseLatexCommandAt(source, index);
4827
+ const trailingName = trailingCommand && trailingCommand.name
4828
+ ? String(trailingCommand.name || "").replace(/\*$/, "").toLowerCase()
4829
+ : "";
4830
+ if (!trailingCommand || !LATEX_PREVIEW_HIDDEN_COMMANDS.has(trailingName)) {
4831
+ break;
4832
+ }
4833
+ let nextIndex = skipLatexPreviewCommentSpace(source, trailingCommand.end);
4834
+ if (source[nextIndex] === "[") {
4835
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4836
+ if (optionalGroup) {
4837
+ nextIndex = skipLatexPreviewCommentSpace(source, optionalGroup.end);
4838
+ }
4839
+ }
4840
+ if (source[nextIndex] === "{") {
4841
+ const argGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4842
+ if (argGroup) {
4843
+ nextIndex = skipLatexPreviewCommentSpace(source, argGroup.end);
4844
+ }
4845
+ }
4846
+ index = nextIndex;
4847
+ }
4848
+ if (skipLatexPreviewCommentSpace(source, index) < source.length) return null;
4849
+ return {
4850
+ commandName,
4851
+ titleText: source.slice(titleGroup.contentStart, titleGroup.contentEnd),
4852
+ };
4853
+ }
4854
+
4855
+ function extractLatexPreviewVisibleText(text) {
4856
+ const source = String(text || "");
4857
+ let out = "";
4858
+ let index = 0;
4859
+
4860
+ while (index < source.length) {
4861
+ const ch = source[index];
4862
+ if (ch === "%" && !isEscapedAt(source, index)) {
4863
+ while (index < source.length && source[index] !== "\n") index += 1;
4864
+ continue;
4865
+ }
4866
+ if (source.startsWith("$$", index)) {
4867
+ const close = source.indexOf("$$", index + 2);
4868
+ if (close >= 0) {
4869
+ out += " " + source.slice(index + 2, close) + " ";
4870
+ index = close + 2;
4871
+ continue;
4872
+ }
4873
+ }
4874
+ if (ch === "$" && !isEscapedAt(source, index)) {
4875
+ const close = findClosingUnescapedSequence(source, index + 1, "$", true);
4876
+ if (close >= 0) {
4877
+ out += " " + source.slice(index + 1, close) + " ";
4878
+ index = close + 1;
4879
+ continue;
4880
+ }
4881
+ }
4882
+ if (source.startsWith("\\(", index)) {
4883
+ const close = source.indexOf("\\)", index + 2);
4884
+ if (close >= 0) {
4885
+ out += " " + source.slice(index + 2, close) + " ";
4886
+ index = close + 2;
4887
+ continue;
4888
+ }
4889
+ }
4890
+ if (source.startsWith("\\[", index)) {
4891
+ const close = source.indexOf("\\]", index + 2);
4892
+ if (close >= 0) {
4893
+ out += " " + source.slice(index + 2, close) + " ";
4894
+ index = close + 2;
4895
+ continue;
4896
+ }
4897
+ }
4898
+ if (source.startsWith("\\begin{", index)) {
4899
+ const envGroup = readBalancedLatexGroup(source, index + 6, "{", "}");
4900
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim() : "";
4901
+ if (envName && DISPLAY_MATH_ENV_NAMES.has(envName)) {
4902
+ const closeToken = "\\end{" + envName + "}";
4903
+ const close = source.indexOf(closeToken, envGroup.end);
4904
+ if (close >= 0) {
4905
+ out += " " + source.slice(envGroup.end, close) + " ";
4906
+ index = close + closeToken.length;
4907
+ continue;
4908
+ }
4909
+ }
4910
+ }
4911
+ if (source.startsWith("\\end{", index)) {
4912
+ const envGroup = readBalancedLatexGroup(source, index + 4, "{", "}");
4913
+ if (envGroup) {
4914
+ index = envGroup.end;
4915
+ continue;
4916
+ }
4917
+ }
4918
+ if (ch === "\\") {
4919
+ const command = parseLatexCommandAt(source, index);
4920
+ const commandName = command && command.name
4921
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
4922
+ : "";
4923
+ if (!command) {
4924
+ index += 1;
4925
+ continue;
4926
+ }
4927
+ if (commandName === "begin" || commandName === "end") {
4928
+ let nextIndex = skipLatexWhitespace(source, command.end);
4929
+ if (source[nextIndex] === "{") {
4930
+ const group = readBalancedLatexGroup(source, nextIndex, "{", "}");
4931
+ if (group) {
4932
+ index = group.end;
4933
+ continue;
4934
+ }
4935
+ }
4936
+ }
4937
+ if (commandName === "latex") {
4938
+ out += "LaTeX";
4939
+ index = command.end;
4940
+ continue;
4941
+ }
4942
+ if (commandName === "tex") {
4943
+ out += "TeX";
4944
+ index = command.end;
4945
+ continue;
4946
+ }
4947
+ if (commandName === "item") {
4948
+ out += " ";
4949
+ index = command.end;
4950
+ continue;
4951
+ }
4952
+ let nextIndex = skipLatexWhitespace(source, command.end);
4953
+ if (source[nextIndex] === "[") {
4954
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4955
+ if (optionalGroup) {
4956
+ nextIndex = skipLatexWhitespace(source, optionalGroup.end);
4957
+ }
4958
+ }
4959
+ if (LATEX_PREVIEW_VISIBLE_GROUP_COMMANDS.has(commandName) && source[nextIndex] === "{") {
4960
+ const group = readBalancedLatexGroup(source, nextIndex, "{", "}");
4961
+ if (group) {
4962
+ out += " " + extractLatexPreviewVisibleText(source.slice(group.contentStart, group.contentEnd)) + " ";
4963
+ index = group.end;
4964
+ continue;
4965
+ }
4966
+ }
4967
+ if (LATEX_PREVIEW_SECOND_ARG_VISIBLE_COMMANDS.has(commandName) && source[nextIndex] === "{") {
4968
+ const firstGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4969
+ if (firstGroup) {
4970
+ let secondIndex = skipLatexWhitespace(source, firstGroup.end);
4971
+ if (source[secondIndex] === "{") {
4972
+ const secondGroup = readBalancedLatexGroup(source, secondIndex, "{", "}");
4973
+ if (secondGroup) {
4974
+ out += " " + extractLatexPreviewVisibleText(source.slice(secondGroup.contentStart, secondGroup.contentEnd)) + " ";
4975
+ index = secondGroup.end;
4976
+ continue;
4977
+ }
4978
+ }
4979
+ }
4980
+ }
4981
+ if (LATEX_PREVIEW_HIDDEN_COMMANDS.has(commandName)) {
4982
+ index = nextIndex;
4983
+ if (source[index] === "{") {
4984
+ const group = readBalancedLatexGroup(source, index, "{", "}");
4985
+ if (group) {
4986
+ index = group.end;
4987
+ continue;
4988
+ }
4989
+ }
4990
+ index = command.end;
4991
+ continue;
4992
+ }
4993
+ index = command.end;
4994
+ continue;
4995
+ }
4996
+ if (ch === "{" || ch === "}") {
4997
+ index += 1;
4998
+ continue;
4999
+ }
5000
+ if (ch === "~") {
5001
+ out += " ";
5002
+ index += 1;
5003
+ continue;
5004
+ }
5005
+ out += ch;
5006
+ index += 1;
5007
+ }
5008
+
5009
+ return normalizeVisiblePreviewText(out);
5010
+ }
5011
+
5012
+ function findLatexDocumentBodyRange(text) {
5013
+ const source = String(text || "");
5014
+ const beginMatch = source.match(/\\begin\{document\}/);
5015
+ if (!beginMatch || beginMatch.index == null) {
5016
+ return { start: 0, end: source.length };
5017
+ }
5018
+ const start = beginMatch.index + beginMatch[0].length;
5019
+ const endMatch = source.slice(start).match(/\\end\{document\}/);
5020
+ return {
5021
+ start,
5022
+ end: endMatch && endMatch.index != null ? (start + endMatch.index) : source.length,
5023
+ };
5024
+ }
5025
+
5026
+ function normalizeLatexPreviewBlockText(blockText, kind) {
5027
+ const source = String(blockText || "");
5028
+ if (/\\(?:bibliography|printbibliography)\b/i.test(source)) {
5029
+ return kind === "heading" ? "References" : "references";
5030
+ }
5031
+ if (kind === "math") {
5032
+ const mathRange = getStandaloneDisplayMathRange(stripLatexPreviewComments(source));
5033
+ return mathRange ? normalizeVisiblePreviewText(mathRange.bodyText) : normalizeVisiblePreviewText(source);
5034
+ }
5035
+ if (kind === "heading") {
5036
+ const heading = readLatexHeadingChunk(stripLatexPreviewComments(source));
5037
+ return heading ? extractLatexPreviewVisibleText(heading.titleText) : extractLatexPreviewVisibleText(source);
5038
+ }
5039
+ return extractLatexPreviewVisibleText(source);
5040
+ }
5041
+
5042
+ function isLatexPreviewSkippableChunk(chunkText) {
5043
+ const source = stripLatexPreviewComments(chunkText).trim();
5044
+ if (!source) return true;
5045
+ const command = parseLatexCommandAt(source, 0);
5046
+ const commandName = command && command.name
5047
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
5048
+ : "";
5049
+ if (command && LATEX_PREVIEW_HIDDEN_COMMANDS.has(commandName)) return true;
5050
+ if (command && /^(?:documentclass|usepackage|newtheorem|title|author|date|maketitle|tableofcontents)$/i.test(commandName)) return true;
5051
+ if (source.startsWith("\\begin{")) {
5052
+ const envGroup = readBalancedLatexGroup(source, 6, "{", "}");
5053
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim().toLowerCase() : "";
5054
+ if (envName && LATEX_PREVIEW_SKIPPED_ENV_NAMES.has(envName)) return true;
5055
+ }
5056
+ return false;
5057
+ }
5058
+
4673
5059
  function normalizePreviewComparableCharacter(character) {
4674
5060
  switch (String(character || "")) {
4675
5061
  case "\u2018":
@@ -5102,12 +5488,12 @@
5102
5488
 
5103
5489
  function getPreviewMathSearchText(element) {
5104
5490
  if (!element || !(element instanceof Element)) return null;
5491
+ const texSourceAttr = element.getAttribute("data-tex-source");
5492
+ if (texSourceAttr && texSourceAttr.trim()) {
5493
+ return texSourceAttr;
5494
+ }
5105
5495
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
5106
5496
  if (tag === "MATH") {
5107
- const texSource = element.getAttribute("data-tex-source");
5108
- if (texSource && texSource.trim()) {
5109
- return texSource;
5110
- }
5111
5497
  return typeof element.textContent === "string" ? element.textContent : "";
5112
5498
  }
5113
5499
  if (element.classList && element.classList.contains("math") && (element.classList.contains("inline") || element.classList.contains("display"))) {
@@ -5116,6 +5502,16 @@
5116
5502
  element.classList.contains("display"),
5117
5503
  );
5118
5504
  }
5505
+ if (
5506
+ element.classList
5507
+ && (element.classList.contains("studio-display-equation") || element.classList.contains("studio-display-equation-body"))
5508
+ && typeof element.querySelector === "function"
5509
+ ) {
5510
+ const innerMathEl = element.querySelector("[data-tex-source], math[display='block'], .studio-mathjax-fallback-display");
5511
+ if (innerMathEl && innerMathEl !== element) {
5512
+ return getPreviewMathSearchText(innerMathEl);
5513
+ }
5514
+ }
5119
5515
  return null;
5120
5516
  }
5121
5517
 
@@ -5277,8 +5673,9 @@
5277
5673
  }
5278
5674
 
5279
5675
  function scanSourcePreviewCommentBlocks(markdown) {
5280
- if (editorLanguage !== "markdown") return [];
5281
- return scanMarkdownPreviewCommentBlocks(markdown);
5676
+ if (editorLanguage === "markdown") return scanMarkdownPreviewCommentBlocks(markdown);
5677
+ if (editorLanguage === "latex") return scanLatexPreviewCommentBlocks(markdown);
5678
+ return [];
5282
5679
  }
5283
5680
 
5284
5681
  function scanMarkdownPreviewCommentBlocks(markdown) {
@@ -5497,8 +5894,199 @@
5497
5894
  return expandSourcePreviewCommentBlocksByDisplayMath(source, blocks);
5498
5895
  }
5499
5896
 
5897
+ function scanLatexPreviewCommentBlocks(markdown) {
5898
+ const source = String(markdown || "").replace(/\r\n/g, "\n");
5899
+ if (!source) return [];
5900
+ const bodyRange = findLatexDocumentBodyRange(source);
5901
+ const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
5902
+ const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
5903
+ const bodyText = source.slice(bodyStart, bodyEnd);
5904
+ const lines = bodyText.split("\n");
5905
+ const lineOffsets = [];
5906
+ let runningOffset = 0;
5907
+ for (const line of lines) {
5908
+ lineOffsets.push(runningOffset);
5909
+ runningOffset += line.length + 1;
5910
+ }
5911
+
5912
+ function getLine(index) {
5913
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
5914
+ }
5915
+
5916
+ function getStrippedLine(index) {
5917
+ return stripLatexPreviewComments(getLine(index)).trim();
5918
+ }
5919
+
5920
+ function isBlankLine(index) {
5921
+ return !getStrippedLine(index);
5922
+ }
5923
+
5924
+ function isBibliographyCommandLine(index) {
5925
+ return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
5926
+ }
5927
+
5928
+ function makeBlock(kind, startLineIndex, endLineIndex) {
5929
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
5930
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
5931
+ const start = bodyStart + (lineOffsets[safeStartLine] || 0);
5932
+ const end = bodyStart + (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
5933
+ return {
5934
+ kind,
5935
+ start,
5936
+ end,
5937
+ lineStart: getLineNumberAtOffset(source, start),
5938
+ lineEnd: getLineNumberAtOffset(source, Math.max(start, end - 1)),
5939
+ };
5940
+ }
5941
+
5942
+ function getChunkText(startLineIndex, endLineIndex) {
5943
+ return bodyText.slice(
5944
+ lineOffsets[startLineIndex] || 0,
5945
+ (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
5946
+ );
5947
+ }
5948
+
5949
+ function getEnvironmentStartName(index) {
5950
+ const line = getStrippedLine(index);
5951
+ const match = line.match(/^\\begin\{([^}]+)\}/);
5952
+ return match ? String(match[1] || "").trim().toLowerCase() : "";
5953
+ }
5954
+
5955
+ function findEnvironmentEndLine(startLineIndex, envName) {
5956
+ const openToken = "\\begin{" + envName + "}";
5957
+ const closeToken = "\\end{" + envName + "}";
5958
+ let depth = 0;
5959
+ for (let lineIndex = startLineIndex; lineIndex < lines.length; lineIndex += 1) {
5960
+ const line = getStrippedLine(lineIndex);
5961
+ if (line.includes(openToken)) depth += 1;
5962
+ if (line.includes(closeToken)) {
5963
+ depth -= 1;
5964
+ if (depth <= 0) return lineIndex;
5965
+ }
5966
+ }
5967
+ return startLineIndex;
5968
+ }
5969
+
5970
+ function isHeadingLine(index) {
5971
+ return Boolean(readLatexHeadingChunk(getLine(index)));
5972
+ }
5973
+
5974
+ function findBibliographyCommandEndLine(startLineIndex) {
5975
+ let endLineIndex = startLineIndex;
5976
+ for (let lineIndex = startLineIndex + 1; lineIndex < lines.length; lineIndex += 1) {
5977
+ if (!isBibliographyCommandLine(lineIndex)) break;
5978
+ endLineIndex = lineIndex;
5979
+ }
5980
+ return endLineIndex;
5981
+ }
5982
+
5983
+ function isMathStartLine(index) {
5984
+ const line = getStrippedLine(index);
5985
+ if (!line) return false;
5986
+ if (line.startsWith("$$") || line.startsWith("\\[")) return true;
5987
+ const envName = getEnvironmentStartName(index);
5988
+ return Boolean(envName && DISPLAY_MATH_ENV_NAMES.has(envName));
5989
+ }
5990
+
5991
+ function findMathEndLine(startLineIndex) {
5992
+ for (let endLineIndex = startLineIndex; endLineIndex < lines.length; endLineIndex += 1) {
5993
+ const chunkText = getChunkText(startLineIndex, endLineIndex);
5994
+ if (getStandaloneDisplayMathRange(stripLatexPreviewComments(chunkText))) {
5995
+ return endLineIndex;
5996
+ }
5997
+ }
5998
+ return startLineIndex;
5999
+ }
6000
+
6001
+ const blocks = [];
6002
+ let lineIndex = 0;
6003
+ while (lineIndex < lines.length) {
6004
+ if (isBlankLine(lineIndex)) {
6005
+ lineIndex += 1;
6006
+ continue;
6007
+ }
6008
+
6009
+ const strippedLine = getStrippedLine(lineIndex);
6010
+ const envName = getEnvironmentStartName(lineIndex);
6011
+
6012
+ if (isHeadingLine(lineIndex)) {
6013
+ blocks.push(makeBlock("heading", lineIndex, lineIndex));
6014
+ lineIndex += 1;
6015
+ continue;
6016
+ }
6017
+
6018
+ if (envName === "abstract" || envName === "keywords") {
6019
+ const endLineIndex = findEnvironmentEndLine(lineIndex, envName);
6020
+ const chunkText = getChunkText(lineIndex, endLineIndex);
6021
+ if (normalizeLatexPreviewBlockText(chunkText, "paragraph")) {
6022
+ blocks.push(makeBlock("paragraph", lineIndex, endLineIndex));
6023
+ }
6024
+ lineIndex = endLineIndex + 1;
6025
+ continue;
6026
+ }
6027
+
6028
+ if (envName && LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME.has(envName)) {
6029
+ const endLineIndex = findEnvironmentEndLine(lineIndex, envName);
6030
+ blocks.push(makeBlock(LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME.get(envName) || "paragraph", lineIndex, endLineIndex));
6031
+ lineIndex = endLineIndex + 1;
6032
+ continue;
6033
+ }
6034
+
6035
+ if (isBibliographyCommandLine(lineIndex)) {
6036
+ const endLineIndex = findBibliographyCommandEndLine(lineIndex);
6037
+ blocks.push(makeBlock("heading", lineIndex, endLineIndex));
6038
+ blocks.push(makeBlock("paragraph", lineIndex, endLineIndex));
6039
+ lineIndex = endLineIndex + 1;
6040
+ continue;
6041
+ }
6042
+
6043
+ if (envName && LATEX_PREVIEW_SKIPPED_ENV_NAMES.has(envName) && !DISPLAY_MATH_ENV_NAMES.has(envName)) {
6044
+ lineIndex = findEnvironmentEndLine(lineIndex, envName) + 1;
6045
+ continue;
6046
+ }
6047
+
6048
+ if (isMathStartLine(lineIndex)) {
6049
+ const endLineIndex = findMathEndLine(lineIndex);
6050
+ blocks.push(makeBlock("math", lineIndex, endLineIndex));
6051
+ lineIndex = endLineIndex + 1;
6052
+ continue;
6053
+ }
6054
+
6055
+ if (isLatexPreviewSkippableChunk(strippedLine)) {
6056
+ lineIndex += 1;
6057
+ continue;
6058
+ }
6059
+
6060
+ const paragraphStartLine = lineIndex;
6061
+ let paragraphEndLine = lineIndex;
6062
+ for (let nextLineIndex = lineIndex + 1; nextLineIndex < lines.length; nextLineIndex += 1) {
6063
+ if (isBlankLine(nextLineIndex) || isHeadingLine(nextLineIndex) || isMathStartLine(nextLineIndex)) {
6064
+ break;
6065
+ }
6066
+ const nextEnvName = getEnvironmentStartName(nextLineIndex);
6067
+ if (nextEnvName) {
6068
+ break;
6069
+ }
6070
+ paragraphEndLine = nextLineIndex;
6071
+ }
6072
+
6073
+ const chunkText = getChunkText(paragraphStartLine, paragraphEndLine);
6074
+ if (normalizeLatexPreviewBlockText(chunkText, "paragraph") && !isLatexPreviewSkippableChunk(chunkText)) {
6075
+ blocks.push(makeBlock("paragraph", paragraphStartLine, paragraphEndLine));
6076
+ }
6077
+ lineIndex = paragraphEndLine + 1;
6078
+ }
6079
+
6080
+ return blocks;
6081
+ }
6082
+
5500
6083
  function isPreviewDisplayMathElement(element) {
5501
- return Boolean(element && element instanceof Element && element.matches && element.matches("math[display='block'], .studio-mathjax-fallback-display"));
6084
+ return Boolean(
6085
+ element
6086
+ && element instanceof Element
6087
+ && element.matches
6088
+ && element.matches("math[display='block'], .studio-mathjax-fallback-display, .studio-display-equation, .studio-display-equation-body")
6089
+ );
5502
6090
  }
5503
6091
 
5504
6092
  function previewNodesHaveVisibleContent(nodes) {
@@ -5511,8 +6099,63 @@
5511
6099
  });
5512
6100
  }
5513
6101
 
6102
+ function wrapLoosePreviewInlineRunsAsParagraphs(targetEl) {
6103
+ if (!targetEl || !targetEl.childNodes || typeof document.createElement !== "function") return;
6104
+ const childNodes = Array.from(targetEl.childNodes || []);
6105
+ if (childNodes.length === 0) return;
6106
+
6107
+ function isDirectBlockChild(node) {
6108
+ if (!(node instanceof Element) || node.parentElement !== targetEl) return false;
6109
+ const tag = node.tagName ? node.tagName.toUpperCase() : "";
6110
+ if (/^H[1-6]$/.test(tag)) return true;
6111
+ if (tag === "P" || tag === "BLOCKQUOTE" || tag === "UL" || tag === "OL" || tag === "TABLE" || tag === "PRE" || tag === "HEADER" || tag === "FIGURE") {
6112
+ return true;
6113
+ }
6114
+ if (tag === "MATH") {
6115
+ return String(node.getAttribute("display") || "").toLowerCase() === "block";
6116
+ }
6117
+ if (tag === "DIV") return true;
6118
+ return false;
6119
+ }
6120
+
6121
+ let runNodes = [];
6122
+
6123
+ function flushRun(referenceNode) {
6124
+ if (runNodes.length === 0) return;
6125
+ if (!previewNodesHaveVisibleContent(runNodes)) {
6126
+ runNodes.forEach((node) => {
6127
+ if (node && node.parentNode === targetEl) {
6128
+ targetEl.removeChild(node);
6129
+ }
6130
+ });
6131
+ runNodes = [];
6132
+ return;
6133
+ }
6134
+ const paragraphEl = document.createElement("p");
6135
+ runNodes.forEach((node) => {
6136
+ paragraphEl.appendChild(node);
6137
+ });
6138
+ targetEl.insertBefore(paragraphEl, referenceNode || null);
6139
+ runNodes = [];
6140
+ }
6141
+
6142
+ childNodes.forEach((node) => {
6143
+ if (node instanceof Element && isDirectBlockChild(node)) {
6144
+ flushRun(node);
6145
+ return;
6146
+ }
6147
+ if (node.parentNode === targetEl) {
6148
+ runNodes.push(node);
6149
+ }
6150
+ });
6151
+ flushRun(null);
6152
+ }
6153
+
5514
6154
  function splitMixedPreviewParagraphsAroundDisplayMath(targetEl) {
5515
6155
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6156
+ if (editorLanguage === "latex") {
6157
+ wrapLoosePreviewInlineRunsAsParagraphs(targetEl);
6158
+ }
5516
6159
  Array.from(targetEl.querySelectorAll("p")).forEach((paragraphEl) => {
5517
6160
  if (!(paragraphEl instanceof Element) || !paragraphEl.parentNode) return;
5518
6161
  if (paragraphEl.closest && paragraphEl.closest(".preview-comment-block")) return;
@@ -5574,6 +6217,20 @@
5574
6217
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
5575
6218
  if (/^H[1-6]$/.test(tag)) return "heading";
5576
6219
  if (tag === "P") return "paragraph";
6220
+ if (tag === "FIGURE") {
6221
+ if (element.classList && element.classList.contains("studio-algorithm-block")) {
6222
+ return "algorithm";
6223
+ }
6224
+ return editorLanguage === "latex" ? "figure" : "";
6225
+ }
6226
+ if (tag === "DIV" && element.classList) {
6227
+ if (element.classList.contains("studio-display-equation")) {
6228
+ return "math";
6229
+ }
6230
+ if (element.classList.contains("abstract") || element.classList.contains("keywords") || element.classList.contains("references")) {
6231
+ return "paragraph";
6232
+ }
6233
+ }
5577
6234
  if (tag === "BLOCKQUOTE") return "blockquote";
5578
6235
  if (tag === "UL" || tag === "OL") return "list";
5579
6236
  if (tag === "TABLE") return "table";
@@ -5605,11 +6262,49 @@
5605
6262
  return Boolean(getPreviewCommentTargetKind(element));
5606
6263
  }
5607
6264
 
6265
+ function isLatexPreviewCommentTargetElement(element, targetEl) {
6266
+ if (!element || !(element instanceof Element) || !targetEl) return false;
6267
+ const kind = getPreviewCommentTargetKind(element);
6268
+ if (kind === "heading" || kind === "paragraph" || kind === "figure" || kind === "algorithm" || kind === "table") {
6269
+ if (element.parentElement === targetEl) return true;
6270
+ if (
6271
+ kind === "paragraph"
6272
+ && element.classList
6273
+ && element.classList.contains("abstract")
6274
+ && element.parentElement
6275
+ && element.parentElement.tagName === "HEADER"
6276
+ && element.parentElement.id === "title-block-header"
6277
+ && element.parentElement.parentElement === targetEl
6278
+ ) {
6279
+ return true;
6280
+ }
6281
+ return false;
6282
+ }
6283
+ if (kind === "math") {
6284
+ if (element.parentElement === targetEl) return true;
6285
+ const bodyEl = element.parentElement;
6286
+ const frameEl = bodyEl && bodyEl.parentElement;
6287
+ return Boolean(
6288
+ bodyEl
6289
+ && bodyEl.classList
6290
+ && bodyEl.classList.contains("studio-display-equation-body")
6291
+ && frameEl
6292
+ && frameEl.classList
6293
+ && frameEl.classList.contains("studio-display-equation")
6294
+ && frameEl.parentElement === targetEl
6295
+ );
6296
+ }
6297
+ return false;
6298
+ }
6299
+
5608
6300
  function collectPreviewCommentTargetElements(targetEl) {
5609
6301
  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";
6302
+ 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
6303
  return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
5612
6304
  if (!isPreviewCommentTargetElement(element)) return false;
6305
+ if (editorLanguage === "latex" && !isLatexPreviewCommentTargetElement(element, targetEl)) {
6306
+ return false;
6307
+ }
5613
6308
  let ancestor = element.parentElement;
5614
6309
  while (ancestor && ancestor !== targetEl) {
5615
6310
  if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
@@ -5626,6 +6321,9 @@
5626
6321
  function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
5627
6322
  if (!sourceBlock) return "";
5628
6323
  const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
6324
+ if (editorLanguage === "latex") {
6325
+ return normalizeLatexPreviewBlockText(blockText, sourceBlock.kind);
6326
+ }
5629
6327
  if (sourceBlock.kind === "page-break") {
5630
6328
  const match = blockText.trim().match(/^\\(newpage|pagebreak|clearpage)/i);
5631
6329
  return match ? String(match[1] || "").toLowerCase() : "page-break";
@@ -5673,13 +6371,46 @@
5673
6371
  return longer.includes(shorter);
5674
6372
  }
5675
6373
 
6374
+ function tokenizePreviewComparableText(text) {
6375
+ return normalizeVisiblePreviewText(text)
6376
+ .toLowerCase()
6377
+ .split(/\s+/)
6378
+ .map((token) => token.replace(/^[^0-9A-Za-z\u00C0-\uFFFF]+|[^0-9A-Za-z\u00C0-\uFFFF]+$/g, ""))
6379
+ .filter((token) => token && (token.length >= 4 || /[A-Za-z\u00C0-\uFFFF]/.test(token)));
6380
+ }
6381
+
6382
+ function getHighConfidenceLatexOrderedTokenMatchScore(targetText, desiredText) {
6383
+ if (editorLanguage !== "latex") return -1;
6384
+ const targetTokens = tokenizePreviewComparableText(targetText);
6385
+ const desiredTokens = tokenizePreviewComparableText(desiredText);
6386
+ if (targetTokens.length === 0 || desiredTokens.length < 5) return -1;
6387
+
6388
+ let targetTokenIndex = 0;
6389
+ let matchedCount = 0;
6390
+ for (const token of desiredTokens) {
6391
+ while (targetTokenIndex < targetTokens.length && targetTokens[targetTokenIndex] !== token) {
6392
+ targetTokenIndex += 1;
6393
+ }
6394
+ if (targetTokenIndex >= targetTokens.length) break;
6395
+ matchedCount += 1;
6396
+ targetTokenIndex += 1;
6397
+ }
6398
+
6399
+ const matchRatio = matchedCount / desiredTokens.length;
6400
+ if (matchedCount < 5 || matchRatio < 0.6) return -1;
6401
+ return matchedCount * 1000 + Math.round(matchRatio * 100);
6402
+ }
6403
+
5676
6404
  function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5677
6405
  const desiredKind = sourceBlock ? sourceBlock.kind : "";
5678
6406
  const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
6407
+ const preferredStartIndex = Math.max(0, startIndex || 0);
5679
6408
  let fallbackIndex = -1;
5680
6409
  let containsIndex = -1;
6410
+ let orderedTokenIndex = -1;
6411
+ let orderedTokenScore = Number.NEGATIVE_INFINITY;
5681
6412
 
5682
- for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
6413
+ for (let i = preferredStartIndex; i < targetBlocks.length; i += 1) {
5683
6414
  const targetEntry = targetBlocks[i];
5684
6415
  if (!targetEntry || targetEntry.kind !== desiredKind) continue;
5685
6416
  if (fallbackIndex < 0) fallbackIndex = i;
@@ -5691,10 +6422,19 @@
5691
6422
  if (containsIndex < 0 && isHighConfidencePreviewTextContainmentMatch(targetText, desiredText)) {
5692
6423
  containsIndex = i;
5693
6424
  }
6425
+ const latexTokenScore = getHighConfidenceLatexOrderedTokenMatchScore(targetText, desiredText);
6426
+ if (latexTokenScore >= 0) {
6427
+ const score = latexTokenScore - (Math.abs(i - preferredStartIndex) * 4);
6428
+ if (score > orderedTokenScore) {
6429
+ orderedTokenScore = score;
6430
+ orderedTokenIndex = i;
6431
+ }
6432
+ }
5694
6433
  }
5695
6434
  }
5696
6435
 
5697
6436
  if (containsIndex >= 0) return containsIndex;
6437
+ if (orderedTokenIndex >= 0) return orderedTokenIndex;
5698
6438
  return fallbackIndex;
5699
6439
  }
5700
6440
 
@@ -5713,6 +6453,7 @@
5713
6453
  const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5714
6454
  const summaryBtn = blockEl.querySelector(".preview-comment-summary");
5715
6455
  const addBtn = blockEl.querySelector(".preview-comment-add");
6456
+ const jumpBtn = blockEl.querySelector(".preview-comment-jump");
5716
6457
  const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
5717
6458
  const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
5718
6459
  const blockKey = getPreviewCommentBlockKey(blockEl);
@@ -5736,6 +6477,16 @@
5736
6477
  : "";
5737
6478
  addBtn.setAttribute("aria-label", addBtn.title || "Comment");
5738
6479
  }
6480
+
6481
+ if (jumpBtn) {
6482
+ jumpBtn.hidden = !hasSelection;
6483
+ jumpBtn.textContent = "Jump";
6484
+ jumpBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
6485
+ jumpBtn.title = hasSelection
6486
+ ? ("Jump to the current preview selection on this " + blockKindLabel + " in the raw editor (" + lineLabel + ").")
6487
+ : "";
6488
+ jumpBtn.setAttribute("aria-label", jumpBtn.title || "Jump");
6489
+ }
5739
6490
  }
5740
6491
 
5741
6492
  function updatePreviewCommentBlocksForElement(targetEl) {
@@ -5783,9 +6534,17 @@
5783
6534
  const addBtn = document.createElement("button");
5784
6535
  addBtn.type = "button";
5785
6536
  addBtn.className = "preview-comment-add";
6537
+ addBtn.dataset.previewCommentAction = "comment";
5786
6538
  addBtn.textContent = "Comment";
5787
6539
  controls.appendChild(addBtn);
5788
6540
 
6541
+ const jumpBtn = document.createElement("button");
6542
+ jumpBtn.type = "button";
6543
+ jumpBtn.className = "preview-comment-jump";
6544
+ jumpBtn.dataset.previewCommentAction = "jump";
6545
+ jumpBtn.textContent = "Jump";
6546
+ controls.appendChild(jumpBtn);
6547
+
5789
6548
  originalElement.replaceWith(wrapper);
5790
6549
  wrapper.appendChild(controls);
5791
6550
  originalElement.classList.add("preview-comment-block-content");
@@ -5845,6 +6604,19 @@
5845
6604
  };
5846
6605
  }
5847
6606
 
6607
+ if (editorLanguage === "latex") {
6608
+ const selectedDisplayText = buildNormalizedPreviewRangeText(range);
6609
+ if (!selectedDisplayText) return null;
6610
+ return {
6611
+ selectionStart: blockStart,
6612
+ selectionEnd: blockEnd,
6613
+ lineStart: getLineNumberAtOffset(source, blockStart),
6614
+ lineEnd: getLineNumberAtOffset(source, Math.max(blockStart, blockEnd - 1)),
6615
+ selectedText: source.slice(blockStart, blockEnd),
6616
+ selectedDisplayText,
6617
+ };
6618
+ }
6619
+
5848
6620
  const sourceBlockText = source.slice(blockStart, blockEnd);
5849
6621
  const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5850
6622
  if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
@@ -5992,7 +6764,16 @@
5992
6764
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5993
6765
  const range = resolveReviewNoteRange(note, source);
5994
6766
  if (!range) return false;
5995
- const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
6767
+ const rangeBlock = findPreviewCommentBlockForRange(targetEl, range);
6768
+ const selectionText = getPreviewNoteNormalizedSelectionText(note);
6769
+ let blockEl = rangeBlock;
6770
+ if (selectionText) {
6771
+ const rangeContentEl = rangeBlock ? (rangeBlock.querySelector(".preview-comment-block-content") || rangeBlock) : null;
6772
+ const rangeText = rangeContentEl ? buildNormalizedPreviewSearchText(rangeContentEl) : "";
6773
+ if (!rangeText || !rangeText.includes(selectionText)) {
6774
+ blockEl = findPreviewCommentBlockForNoteText(targetEl, note) || rangeBlock;
6775
+ }
6776
+ }
5996
6777
  if (!blockEl) return false;
5997
6778
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5998
6779
  if (String(blockEl.dataset && blockEl.dataset.previewCommentKind || "") === "math") {
@@ -6362,12 +7143,17 @@
6362
7143
  });
6363
7144
  }
6364
7145
 
6365
- function addReviewNoteFromPreviewSelection(blockEl) {
7146
+ function getActivePreviewSelectionAnchorForBlock(blockEl) {
6366
7147
  if (!blockEl) return null;
6367
7148
  const blockKey = getPreviewCommentBlockKey(blockEl);
6368
- const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
7149
+ return activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
6369
7150
  ? activePreviewCommentSelection
6370
7151
  : null;
7152
+ }
7153
+
7154
+ function addReviewNoteFromPreviewSelection(blockEl) {
7155
+ if (!blockEl) return null;
7156
+ const anchor = getActivePreviewSelectionAnchorForBlock(blockEl);
6371
7157
  if (!anchor) {
6372
7158
  setStatus("Select some preview text within a single block first.", "warning");
6373
7159
  return null;
@@ -6442,14 +7228,13 @@
6442
7228
  });
6443
7229
  }
6444
7230
 
6445
- function jumpToReviewNote(noteId) {
6446
- const note = reviewNotes.find((entry) => entry && entry.id === noteId);
6447
- if (!note) return;
7231
+ function jumpToReviewAnchor(anchor, options) {
7232
+ if (!anchor) return false;
6448
7233
  const current = String(sourceTextEl.value || "");
6449
- const range = resolveReviewNoteRange(note, current);
7234
+ const range = resolveReviewNoteRange(anchor, current);
6450
7235
  if (!range) {
6451
- setStatus("Could not find the anchored location for this comment.", "warning");
6452
- return;
7236
+ setStatus((options && options.notFoundStatusMessage) || "Could not find the anchored location.", "warning");
7237
+ return false;
6453
7238
  }
6454
7239
  suppressEditorSelectionComment = true;
6455
7240
  suppressedEditorSelectionStart = range.start;
@@ -6464,9 +7249,47 @@
6464
7249
  : (cb) => window.setTimeout(cb, 16);
6465
7250
  schedule(() => {
6466
7251
  scrollEditorRangeIntoView(range);
6467
- revealReviewNoteInPreview(note);
7252
+ if (options && typeof options.afterJump === "function") {
7253
+ options.afterJump(range);
7254
+ }
6468
7255
  updateEditorSelectionCommentUi();
6469
7256
  });
7257
+ if (!options || options.status !== false) {
7258
+ setStatus((options && options.statusMessage) || "Jumped to anchored location in the editor.", "success");
7259
+ }
7260
+ return true;
7261
+ }
7262
+
7263
+ function jumpToPreviewSelection(blockEl) {
7264
+ if (!blockEl) return false;
7265
+ const anchor = getActivePreviewSelectionAnchorForBlock(blockEl);
7266
+ if (!anchor) {
7267
+ setStatus("Select some preview text within a single block first.", "warning");
7268
+ return false;
7269
+ }
7270
+ const jumped = jumpToReviewAnchor(anchor, {
7271
+ statusMessage: "Jumped to preview selection in the raw editor.",
7272
+ });
7273
+ if (jumped) {
7274
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
7275
+ if (selection && typeof selection.removeAllRanges === "function") {
7276
+ selection.removeAllRanges();
7277
+ }
7278
+ clearPreviewCommentSelection();
7279
+ }
7280
+ return jumped;
7281
+ }
7282
+
7283
+ function jumpToReviewNote(noteId) {
7284
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
7285
+ if (!note) return;
7286
+ jumpToReviewAnchor(note, {
7287
+ status: false,
7288
+ notFoundStatusMessage: "Could not find the anchored location for this comment.",
7289
+ afterJump: () => {
7290
+ revealReviewNoteInPreview(note);
7291
+ },
7292
+ });
6470
7293
  }
6471
7294
 
6472
7295
  function deleteReviewNote(noteId) {
@@ -8399,14 +9222,14 @@
8399
9222
 
8400
9223
  function handlePreviewCommentActionMouseDown(event) {
8401
9224
  const target = event.target;
8402
- const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
9225
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
8403
9226
  if (!actionBtn) return;
8404
9227
  event.preventDefault();
8405
9228
  }
8406
9229
 
8407
9230
  function handlePreviewCommentActionClick(event) {
8408
9231
  const target = event.target;
8409
- const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
9232
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
8410
9233
  if (!actionBtn) return;
8411
9234
  const blockEl = actionBtn.closest(".preview-comment-block");
8412
9235
  if (!blockEl) return;
@@ -8414,6 +9237,11 @@
8414
9237
  event.stopPropagation();
8415
9238
  const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
8416
9239
  if (!mode || !mode.startsWith("selection")) return;
9240
+ const action = String(actionBtn.dataset && actionBtn.dataset.previewCommentAction ? actionBtn.dataset.previewCommentAction : "comment");
9241
+ if (action === "jump") {
9242
+ jumpToPreviewSelection(blockEl);
9243
+ return;
9244
+ }
8417
9245
  addReviewNoteFromPreviewSelection(blockEl);
8418
9246
  }
8419
9247
 
package/client/studio.css CHANGED
@@ -920,18 +920,19 @@
920
920
  position: absolute;
921
921
  top: 0;
922
922
  right: 0;
923
- z-index: 4;
923
+ z-index: 6;
924
924
  display: inline-flex;
925
925
  align-items: center;
926
926
  gap: 8px;
927
- transform: translateY(-0.38rem);
927
+ transform: translateY(-0.46rem);
928
928
  }
929
929
 
930
930
  .preview-comment-summary {
931
931
  display: none !important;
932
932
  }
933
933
 
934
- .preview-comment-add {
934
+ .preview-comment-add,
935
+ .preview-comment-jump {
935
936
  display: inline-flex;
936
937
  align-items: center;
937
938
  justify-content: center;
@@ -950,14 +951,17 @@
950
951
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
951
952
  }
952
953
 
953
- .preview-comment-block.has-selection .preview-comment-add {
954
+ .preview-comment-block.has-selection .preview-comment-add,
955
+ .preview-comment-block.has-selection .preview-comment-jump {
954
956
  opacity: 1;
955
957
  pointer-events: auto;
956
958
  transform: translateY(0);
957
959
  }
958
960
 
959
961
  .preview-comment-add:hover,
960
- .preview-comment-add:focus-visible {
962
+ .preview-comment-add:focus-visible,
963
+ .preview-comment-jump:hover,
964
+ .preview-comment-jump:focus-visible {
961
965
  background: var(--accent-soft-strong);
962
966
  color: var(--accent);
963
967
  border-color: var(--accent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.49",
3
+ "version": "0.5.50",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",