pi-studio 0.5.51 → 0.5.52
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 +12 -0
- package/README.md +1 -0
- package/client/studio-client.js +604 -1
- package/client/studio.css +72 -9
- package/index.ts +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.52] — 2026-04-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Studio now includes a docked **Outline** rail for the current editor text, with clickable structure entries that jump in the raw editor and, when possible, reveal the matching preview location.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Outline scanning now provides initial structure support for Markdown headings, LaTeX sectioning commands and references, Python classes/functions, JavaScript/TypeScript classes and functions, Julia modules/structs/functions/macros, Bash functions, and diff file/hunk structure.
|
|
14
|
+
- Standalone preview-to-editor jumps now keep the raw-editor selection focused more reliably, so the left-side highlight persists more like comment-card jumps.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- The **Outline** button now only shows its active styling while the outline rail is actually open.
|
|
18
|
+
|
|
7
19
|
## [0.5.51] — 2026-04-09
|
|
8
20
|
|
|
9
21
|
### Added
|
package/README.md
CHANGED
|
@@ -18,6 +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 a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
|
|
21
22
|
- Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with transient **Comment** / **Jump** actions from raw-editor selections plus editor-preview selections for Markdown, LaTeX, and code/text/diff previews, alongside optional inline `[an: ...]` toggles when you want comments reflected in the document text
|
|
22
23
|
- Browses response history (`Prev/Next/Last`) and loads either:
|
|
23
24
|
- response text
|
package/client/studio-client.js
CHANGED
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
105
105
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
106
106
|
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
107
|
+
const outlineBtn = document.getElementById("outlineBtn");
|
|
107
108
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
108
109
|
const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
|
|
109
110
|
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
@@ -114,6 +115,13 @@
|
|
|
114
115
|
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
115
116
|
const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
|
|
116
117
|
const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
|
|
118
|
+
const outlineOverlayEl = document.getElementById("outlineOverlay");
|
|
119
|
+
const outlineDialogEl = document.getElementById("outlineDialog");
|
|
120
|
+
const outlineMetaEl = document.getElementById("outlineMeta");
|
|
121
|
+
const outlineListEl = document.getElementById("outlineList");
|
|
122
|
+
const outlineEmptyStateEl = document.getElementById("outlineEmptyState");
|
|
123
|
+
const outlineCloseBtn = document.getElementById("outlineCloseBtn");
|
|
124
|
+
const outlineDoneBtn = document.getElementById("outlineDoneBtn");
|
|
117
125
|
const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
|
|
118
126
|
const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
|
|
119
127
|
const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
|
|
@@ -305,6 +313,8 @@
|
|
|
305
313
|
let reviewNotesReturnFocusEl = null;
|
|
306
314
|
let reviewNotesPersistTimer = null;
|
|
307
315
|
let reviewNotesLoadNonce = 0;
|
|
316
|
+
let outlineEntries = [];
|
|
317
|
+
let outlineReturnFocusEl = null;
|
|
308
318
|
let pendingReviewNoteFocusId = null;
|
|
309
319
|
let pendingReviewNoteInlineFocusId = null;
|
|
310
320
|
let activePreviewCommentSelection = null;
|
|
@@ -1130,6 +1140,12 @@
|
|
|
1130
1140
|
&& typeof reviewNotesDialogEl.contains === "function"
|
|
1131
1141
|
&& reviewNotesDialogEl.contains(event.target)
|
|
1132
1142
|
);
|
|
1143
|
+
const outlineOwnsEvent = Boolean(
|
|
1144
|
+
outlineDialogEl
|
|
1145
|
+
&& event.target
|
|
1146
|
+
&& typeof outlineDialogEl.contains === "function"
|
|
1147
|
+
&& outlineDialogEl.contains(event.target)
|
|
1148
|
+
);
|
|
1133
1149
|
|
|
1134
1150
|
if (isScratchpadOpen() && plainEscape) {
|
|
1135
1151
|
event.preventDefault();
|
|
@@ -1143,7 +1159,13 @@
|
|
|
1143
1159
|
return;
|
|
1144
1160
|
}
|
|
1145
1161
|
|
|
1146
|
-
if (
|
|
1162
|
+
if (isOutlineOpen() && plainEscape) {
|
|
1163
|
+
event.preventDefault();
|
|
1164
|
+
closeOutline();
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
|
|
1147
1169
|
return;
|
|
1148
1170
|
}
|
|
1149
1171
|
|
|
@@ -2991,6 +3013,7 @@
|
|
|
2991
3013
|
scheduleEditorMetaUpdate();
|
|
2992
3014
|
}
|
|
2993
3015
|
updateEditorSelectionCommentUi();
|
|
3016
|
+
updateOutlineUi();
|
|
2994
3017
|
}
|
|
2995
3018
|
|
|
2996
3019
|
function setEditorView(nextView) {
|
|
@@ -3025,6 +3048,7 @@
|
|
|
3025
3048
|
}
|
|
3026
3049
|
updateReviewNotesUi();
|
|
3027
3050
|
updateEditorSelectionCommentUi();
|
|
3051
|
+
updateOutlineUi();
|
|
3028
3052
|
}
|
|
3029
3053
|
|
|
3030
3054
|
function setRightView(nextView) {
|
|
@@ -4025,6 +4049,10 @@
|
|
|
4025
4049
|
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
4026
4050
|
}
|
|
4027
4051
|
|
|
4052
|
+
function isOutlineOpen() {
|
|
4053
|
+
return Boolean(outlineOverlayEl && !outlineOverlayEl.hidden);
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4028
4056
|
function isReviewNotesOpen() {
|
|
4029
4057
|
return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
|
|
4030
4058
|
}
|
|
@@ -4198,6 +4226,400 @@
|
|
|
4198
4226
|
};
|
|
4199
4227
|
}
|
|
4200
4228
|
|
|
4229
|
+
function buildOutlineLineIndex(text) {
|
|
4230
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
4231
|
+
const lines = source.split("\n");
|
|
4232
|
+
const lineOffsets = [];
|
|
4233
|
+
let runningOffset = 0;
|
|
4234
|
+
for (const line of lines) {
|
|
4235
|
+
lineOffsets.push(runningOffset);
|
|
4236
|
+
runningOffset += line.length + 1;
|
|
4237
|
+
}
|
|
4238
|
+
return { source, lines, lineOffsets };
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
function makeOutlineEntry(options) {
|
|
4242
|
+
const entry = options && typeof options === "object" ? options : {};
|
|
4243
|
+
const label = typeof entry.label === "string" ? entry.label.trim() : "";
|
|
4244
|
+
if (!label) return null;
|
|
4245
|
+
const selectionStart = Math.max(0, Math.floor(Number(entry.selectionStart) || 0));
|
|
4246
|
+
const selectionEnd = Math.max(selectionStart, Math.floor(Number(entry.selectionEnd) || selectionStart));
|
|
4247
|
+
return {
|
|
4248
|
+
id: typeof entry.id === "string" && entry.id ? entry.id : makeRequestId(),
|
|
4249
|
+
kind: typeof entry.kind === "string" && entry.kind ? entry.kind : "section",
|
|
4250
|
+
depth: Math.max(1, Math.floor(Number(entry.depth) || 1)),
|
|
4251
|
+
label,
|
|
4252
|
+
lineStart: Math.max(1, Math.floor(Number(entry.lineStart) || 1)),
|
|
4253
|
+
lineEnd: Math.max(Math.max(1, Math.floor(Number(entry.lineStart) || 1)), Math.floor(Number(entry.lineEnd) || Math.max(1, Math.floor(Number(entry.lineStart) || 1)))),
|
|
4254
|
+
selectionStart,
|
|
4255
|
+
selectionEnd,
|
|
4256
|
+
selectedText: typeof entry.selectedText === "string" ? entry.selectedText : "",
|
|
4257
|
+
selectedDisplayText: typeof entry.selectedDisplayText === "string" && entry.selectedDisplayText ? entry.selectedDisplayText : label,
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
function getOutlineKindLabel(kind) {
|
|
4262
|
+
switch (String(kind || "")) {
|
|
4263
|
+
case "heading": return "Heading";
|
|
4264
|
+
case "section": return "Section";
|
|
4265
|
+
case "subsection": return "Subsection";
|
|
4266
|
+
case "subsubsection": return "Subsubsection";
|
|
4267
|
+
case "paragraph": return "Paragraph";
|
|
4268
|
+
case "subparagraph": return "Subparagraph";
|
|
4269
|
+
case "class": return "Class";
|
|
4270
|
+
case "function": return "Function";
|
|
4271
|
+
case "interface": return "Interface";
|
|
4272
|
+
case "enum": return "Enum";
|
|
4273
|
+
case "type": return "Type";
|
|
4274
|
+
case "struct": return "Struct";
|
|
4275
|
+
case "module": return "Module";
|
|
4276
|
+
case "macro": return "Macro";
|
|
4277
|
+
case "file": return "File";
|
|
4278
|
+
case "hunk": return "Hunk";
|
|
4279
|
+
default: return "Item";
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
function getOutlineKindBadge(kind) {
|
|
4284
|
+
switch (String(kind || "")) {
|
|
4285
|
+
case "section": return "§";
|
|
4286
|
+
case "subsection": return "§§";
|
|
4287
|
+
case "subsubsection": return "§3";
|
|
4288
|
+
case "paragraph": return "¶";
|
|
4289
|
+
case "subparagraph": return "¶2";
|
|
4290
|
+
case "class": return "class";
|
|
4291
|
+
case "function": return "def";
|
|
4292
|
+
case "interface": return "iface";
|
|
4293
|
+
case "enum": return "enum";
|
|
4294
|
+
case "type": return "type";
|
|
4295
|
+
case "struct": return "struct";
|
|
4296
|
+
case "module": return "mod";
|
|
4297
|
+
case "macro": return "macro";
|
|
4298
|
+
case "file": return "file";
|
|
4299
|
+
case "hunk": return "@@";
|
|
4300
|
+
default: return "#";
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
function scanMarkdownOutlineEntries(text) {
|
|
4305
|
+
const { source, lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4306
|
+
const entries = [];
|
|
4307
|
+
let activeFence = null;
|
|
4308
|
+
|
|
4309
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4310
|
+
const line = String(lines[lineIndex] || "");
|
|
4311
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
4312
|
+
if (fenceMatch) {
|
|
4313
|
+
if (!activeFence) {
|
|
4314
|
+
activeFence = fenceMatch[1];
|
|
4315
|
+
} else if (fenceMatch[1][0] === activeFence[0] && fenceMatch[1].length >= activeFence.length) {
|
|
4316
|
+
activeFence = null;
|
|
4317
|
+
}
|
|
4318
|
+
continue;
|
|
4319
|
+
}
|
|
4320
|
+
if (activeFence) continue;
|
|
4321
|
+
|
|
4322
|
+
const atxMatch = line.match(/^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/);
|
|
4323
|
+
if (atxMatch) {
|
|
4324
|
+
const label = normalizeVisiblePreviewText(atxMatch[2] || "");
|
|
4325
|
+
const entry = makeOutlineEntry({
|
|
4326
|
+
kind: atxMatch[1].length === 1 ? "section" : atxMatch[1].length === 2 ? "subsection" : atxMatch[1].length === 3 ? "subsubsection" : "heading",
|
|
4327
|
+
depth: atxMatch[1].length,
|
|
4328
|
+
label,
|
|
4329
|
+
lineStart: lineIndex + 1,
|
|
4330
|
+
lineEnd: lineIndex + 1,
|
|
4331
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4332
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4333
|
+
selectedText: line,
|
|
4334
|
+
selectedDisplayText: label,
|
|
4335
|
+
});
|
|
4336
|
+
if (entry) entries.push(entry);
|
|
4337
|
+
continue;
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
const nextLine = lineIndex + 1 < lines.length ? String(lines[lineIndex + 1] || "") : "";
|
|
4341
|
+
const setextMatch = nextLine.match(/^ {0,3}(=+|-+)\s*$/);
|
|
4342
|
+
if (setextMatch && normalizeVisiblePreviewText(line)) {
|
|
4343
|
+
const depth = setextMatch[1][0] === "=" ? 1 : 2;
|
|
4344
|
+
const label = normalizeVisiblePreviewText(line);
|
|
4345
|
+
const entry = makeOutlineEntry({
|
|
4346
|
+
kind: depth === 1 ? "section" : "subsection",
|
|
4347
|
+
depth,
|
|
4348
|
+
label,
|
|
4349
|
+
lineStart: lineIndex + 1,
|
|
4350
|
+
lineEnd: lineIndex + 1,
|
|
4351
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4352
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4353
|
+
selectedText: line,
|
|
4354
|
+
selectedDisplayText: label,
|
|
4355
|
+
});
|
|
4356
|
+
if (entry) entries.push(entry);
|
|
4357
|
+
lineIndex += 1;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
return entries;
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
const LATEX_OUTLINE_LEVEL_BY_COMMAND = {
|
|
4365
|
+
part: 1,
|
|
4366
|
+
chapter: 1,
|
|
4367
|
+
section: 1,
|
|
4368
|
+
subsection: 2,
|
|
4369
|
+
subsubsection: 3,
|
|
4370
|
+
paragraph: 4,
|
|
4371
|
+
subparagraph: 5,
|
|
4372
|
+
};
|
|
4373
|
+
|
|
4374
|
+
function scanLatexOutlineEntries(text) {
|
|
4375
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
4376
|
+
const bodyRange = findLatexDocumentBodyRange(source);
|
|
4377
|
+
const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
|
|
4378
|
+
const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
|
|
4379
|
+
const bodyText = source.slice(bodyStart, bodyEnd);
|
|
4380
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(bodyText);
|
|
4381
|
+
const entries = [];
|
|
4382
|
+
|
|
4383
|
+
function getLine(index) {
|
|
4384
|
+
return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
function getStrippedLine(index) {
|
|
4388
|
+
return stripLatexPreviewComments(getLine(index)).trim();
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
function isBibliographyCommandLine(index) {
|
|
4392
|
+
return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4396
|
+
let chunk = getLine(lineIndex);
|
|
4397
|
+
let endLineIndex = lineIndex;
|
|
4398
|
+
let heading = readLatexHeadingChunk(chunk);
|
|
4399
|
+
if (/^\s*\\(?:part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\b/.test(chunk)) {
|
|
4400
|
+
while (!heading && endLineIndex + 1 < lines.length && endLineIndex < lineIndex + 5) {
|
|
4401
|
+
endLineIndex += 1;
|
|
4402
|
+
chunk += "\n" + getLine(endLineIndex);
|
|
4403
|
+
heading = readLatexHeadingChunk(chunk);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
if (heading) {
|
|
4407
|
+
const label = extractLatexPreviewVisibleText(heading.titleText || "");
|
|
4408
|
+
const kind = String(heading.commandName || "section").replace(/\*$/, "").toLowerCase();
|
|
4409
|
+
const entry = makeOutlineEntry({
|
|
4410
|
+
kind,
|
|
4411
|
+
depth: LATEX_OUTLINE_LEVEL_BY_COMMAND[kind] || 1,
|
|
4412
|
+
label,
|
|
4413
|
+
lineStart: lineIndex + 1,
|
|
4414
|
+
lineEnd: endLineIndex + 1,
|
|
4415
|
+
selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
|
|
4416
|
+
selectionEnd: bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
|
|
4417
|
+
selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length),
|
|
4418
|
+
selectedDisplayText: label,
|
|
4419
|
+
});
|
|
4420
|
+
if (entry) entries.push(entry);
|
|
4421
|
+
lineIndex = endLineIndex;
|
|
4422
|
+
continue;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
if (isBibliographyCommandLine(lineIndex)) {
|
|
4426
|
+
let endLine = lineIndex;
|
|
4427
|
+
while (endLine + 1 < lines.length && isBibliographyCommandLine(endLine + 1)) {
|
|
4428
|
+
endLine += 1;
|
|
4429
|
+
}
|
|
4430
|
+
const entry = makeOutlineEntry({
|
|
4431
|
+
kind: "section",
|
|
4432
|
+
depth: 1,
|
|
4433
|
+
label: "References",
|
|
4434
|
+
lineStart: lineIndex + 1,
|
|
4435
|
+
lineEnd: endLine + 1,
|
|
4436
|
+
selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
|
|
4437
|
+
selectionEnd: bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length,
|
|
4438
|
+
selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length),
|
|
4439
|
+
selectedDisplayText: "References",
|
|
4440
|
+
});
|
|
4441
|
+
if (entry) entries.push(entry);
|
|
4442
|
+
lineIndex = endLine;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
return entries;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
function scanPythonOutlineEntries(text) {
|
|
4450
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4451
|
+
const entries = [];
|
|
4452
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4453
|
+
const line = String(lines[lineIndex] || "");
|
|
4454
|
+
const classMatch = line.match(/^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
4455
|
+
const defMatch = line.match(/^(\s*)(?:async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
4456
|
+
const match = classMatch || defMatch;
|
|
4457
|
+
if (!match) continue;
|
|
4458
|
+
const indent = String(match[1] || "").replace(/\t/g, " ").length;
|
|
4459
|
+
const label = String(match[2] || "");
|
|
4460
|
+
const kind = classMatch ? "class" : "function";
|
|
4461
|
+
const entry = makeOutlineEntry({
|
|
4462
|
+
kind,
|
|
4463
|
+
depth: Math.max(1, Math.floor(indent / 4) + 1),
|
|
4464
|
+
label,
|
|
4465
|
+
lineStart: lineIndex + 1,
|
|
4466
|
+
lineEnd: lineIndex + 1,
|
|
4467
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4468
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4469
|
+
selectedText: line,
|
|
4470
|
+
selectedDisplayText: label,
|
|
4471
|
+
});
|
|
4472
|
+
if (entry) entries.push(entry);
|
|
4473
|
+
}
|
|
4474
|
+
return entries;
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
function scanJsLikeOutlineEntries(text) {
|
|
4478
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4479
|
+
const entries = [];
|
|
4480
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4481
|
+
const line = String(lines[lineIndex] || "");
|
|
4482
|
+
const patterns = [
|
|
4483
|
+
{ kind: "class", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4484
|
+
{ kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/) },
|
|
4485
|
+
{ kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/) },
|
|
4486
|
+
{ kind: "interface", match: line.match(/^(\s*)(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4487
|
+
{ kind: "enum", match: line.match(/^(\s*)(?:export\s+)?enum\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4488
|
+
{ kind: "type", match: line.match(/^(\s*)(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4489
|
+
];
|
|
4490
|
+
const found = patterns.find((entry) => entry.match);
|
|
4491
|
+
if (!found || !found.match) continue;
|
|
4492
|
+
const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
|
|
4493
|
+
const label = String(found.match[2] || "");
|
|
4494
|
+
const entry = makeOutlineEntry({
|
|
4495
|
+
kind: found.kind,
|
|
4496
|
+
depth: Math.max(1, Math.floor(indent / 2) + 1),
|
|
4497
|
+
label,
|
|
4498
|
+
lineStart: lineIndex + 1,
|
|
4499
|
+
lineEnd: lineIndex + 1,
|
|
4500
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4501
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4502
|
+
selectedText: line,
|
|
4503
|
+
selectedDisplayText: label,
|
|
4504
|
+
});
|
|
4505
|
+
if (entry) entries.push(entry);
|
|
4506
|
+
}
|
|
4507
|
+
return entries;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
function scanJuliaOutlineEntries(text) {
|
|
4511
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4512
|
+
const entries = [];
|
|
4513
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4514
|
+
const line = String(lines[lineIndex] || "");
|
|
4515
|
+
const patterns = [
|
|
4516
|
+
{ kind: "module", match: line.match(/^(\s*)module\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
|
|
4517
|
+
{ kind: "struct", match: line.match(/^(\s*)(?:mutable\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
|
|
4518
|
+
{ kind: "function", match: line.match(/^(\s*)function\s+([A-Za-z_][A-Za-z0-9_!]*)\s*\(/) },
|
|
4519
|
+
{ kind: "macro", match: line.match(/^(\s*)macro\s+([A-Za-z_][A-Za-z0-9_!]*)\b/) },
|
|
4520
|
+
];
|
|
4521
|
+
const found = patterns.find((entry) => entry.match);
|
|
4522
|
+
if (!found || !found.match) continue;
|
|
4523
|
+
const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
|
|
4524
|
+
const label = String(found.match[2] || "");
|
|
4525
|
+
const entry = makeOutlineEntry({
|
|
4526
|
+
kind: found.kind,
|
|
4527
|
+
depth: Math.max(1, Math.floor(indent / 2) + 1),
|
|
4528
|
+
label,
|
|
4529
|
+
lineStart: lineIndex + 1,
|
|
4530
|
+
lineEnd: lineIndex + 1,
|
|
4531
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4532
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4533
|
+
selectedText: line,
|
|
4534
|
+
selectedDisplayText: label,
|
|
4535
|
+
});
|
|
4536
|
+
if (entry) entries.push(entry);
|
|
4537
|
+
}
|
|
4538
|
+
return entries;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
function scanBashOutlineEntries(text) {
|
|
4542
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4543
|
+
const entries = [];
|
|
4544
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4545
|
+
const line = String(lines[lineIndex] || "");
|
|
4546
|
+
const match = line.match(/^(\s*)(?:function\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*\{/);
|
|
4547
|
+
if (!match) continue;
|
|
4548
|
+
const indent = String(match[1] || "").replace(/\t/g, " ").length;
|
|
4549
|
+
const label = String(match[2] || "");
|
|
4550
|
+
const entry = makeOutlineEntry({
|
|
4551
|
+
kind: "function",
|
|
4552
|
+
depth: Math.max(1, Math.floor(indent / 2) + 1),
|
|
4553
|
+
label,
|
|
4554
|
+
lineStart: lineIndex + 1,
|
|
4555
|
+
lineEnd: lineIndex + 1,
|
|
4556
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4557
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4558
|
+
selectedText: line,
|
|
4559
|
+
selectedDisplayText: label,
|
|
4560
|
+
});
|
|
4561
|
+
if (entry) entries.push(entry);
|
|
4562
|
+
}
|
|
4563
|
+
return entries;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
function scanDiffOutlineEntries(text) {
|
|
4567
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4568
|
+
const entries = [];
|
|
4569
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4570
|
+
const line = String(lines[lineIndex] || "");
|
|
4571
|
+
let kind = "";
|
|
4572
|
+
let label = "";
|
|
4573
|
+
let depth = 1;
|
|
4574
|
+
const fileMatch = line.match(/^diff\s+--git\s+a\/([^\s]+)\s+b\/([^\s]+)/);
|
|
4575
|
+
if (fileMatch) {
|
|
4576
|
+
kind = "file";
|
|
4577
|
+
label = String(fileMatch[2] || fileMatch[1] || "");
|
|
4578
|
+
depth = 1;
|
|
4579
|
+
} else if (/^@@/.test(line)) {
|
|
4580
|
+
kind = "hunk";
|
|
4581
|
+
label = line.replace(/^@@\s*|\s*@@.*$/g, "").trim() || line.trim();
|
|
4582
|
+
depth = 2;
|
|
4583
|
+
}
|
|
4584
|
+
if (!kind || !label) continue;
|
|
4585
|
+
const entry = makeOutlineEntry({
|
|
4586
|
+
kind,
|
|
4587
|
+
depth,
|
|
4588
|
+
label,
|
|
4589
|
+
lineStart: lineIndex + 1,
|
|
4590
|
+
lineEnd: lineIndex + 1,
|
|
4591
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4592
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4593
|
+
selectedText: line,
|
|
4594
|
+
selectedDisplayText: label,
|
|
4595
|
+
});
|
|
4596
|
+
if (entry) entries.push(entry);
|
|
4597
|
+
}
|
|
4598
|
+
return entries;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
function scanOutlineEntries(text, language) {
|
|
4602
|
+
switch (String(language || "").toLowerCase()) {
|
|
4603
|
+
case "markdown":
|
|
4604
|
+
return scanMarkdownOutlineEntries(text);
|
|
4605
|
+
case "latex":
|
|
4606
|
+
return scanLatexOutlineEntries(text);
|
|
4607
|
+
case "python":
|
|
4608
|
+
return scanPythonOutlineEntries(text);
|
|
4609
|
+
case "javascript":
|
|
4610
|
+
case "typescript":
|
|
4611
|
+
return scanJsLikeOutlineEntries(text);
|
|
4612
|
+
case "julia":
|
|
4613
|
+
return scanJuliaOutlineEntries(text);
|
|
4614
|
+
case "bash":
|
|
4615
|
+
return scanBashOutlineEntries(text);
|
|
4616
|
+
case "diff":
|
|
4617
|
+
return scanDiffOutlineEntries(text);
|
|
4618
|
+
default:
|
|
4619
|
+
return [];
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4201
4623
|
function cloneReviewNotes(notes) {
|
|
4202
4624
|
return Array.isArray(notes)
|
|
4203
4625
|
? notes
|
|
@@ -7011,6 +7433,138 @@
|
|
|
7011
7433
|
updateEditorSelectionCommentUi();
|
|
7012
7434
|
}
|
|
7013
7435
|
|
|
7436
|
+
function getOutlineEntriesForCurrentEditor() {
|
|
7437
|
+
return scanOutlineEntries(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "", editorLanguage || "markdown");
|
|
7438
|
+
}
|
|
7439
|
+
|
|
7440
|
+
function updateOutlineUi() {
|
|
7441
|
+
outlineEntries = getOutlineEntriesForCurrentEditor();
|
|
7442
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
7443
|
+
const count = outlineEntries.length;
|
|
7444
|
+
const hasEntries = count > 0;
|
|
7445
|
+
const isOpen = isOutlineOpen();
|
|
7446
|
+
if (outlineBtn) {
|
|
7447
|
+
outlineBtn.textContent = "Outline";
|
|
7448
|
+
outlineBtn.classList.remove("has-content");
|
|
7449
|
+
outlineBtn.classList.toggle("is-active", isOpen);
|
|
7450
|
+
outlineBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
|
|
7451
|
+
outlineBtn.title = isOpen
|
|
7452
|
+
? "Hide document outline."
|
|
7453
|
+
: (hasEntries
|
|
7454
|
+
? (count + " outline entr" + (count === 1 ? "y" : "ies") + " for " + descriptor.label + ". Open the outline rail.")
|
|
7455
|
+
: "Open document outline for the current editor text.");
|
|
7456
|
+
}
|
|
7457
|
+
if (outlineMetaEl) {
|
|
7458
|
+
outlineMetaEl.textContent = hasEntries
|
|
7459
|
+
? (count + " entr" + (count === 1 ? "y" : "ies") + " · " + (editorLanguage || "text") + " · " + descriptor.label)
|
|
7460
|
+
: ("No outline entries · " + (editorLanguage || "text"));
|
|
7461
|
+
}
|
|
7462
|
+
if (outlineDoneBtn) {
|
|
7463
|
+
outlineDoneBtn.disabled = !isOpen;
|
|
7464
|
+
}
|
|
7465
|
+
if (outlineEmptyStateEl) {
|
|
7466
|
+
outlineEmptyStateEl.hidden = hasEntries;
|
|
7467
|
+
}
|
|
7468
|
+
renderOutlineList();
|
|
7469
|
+
}
|
|
7470
|
+
|
|
7471
|
+
function renderOutlineList() {
|
|
7472
|
+
if (!outlineListEl) return;
|
|
7473
|
+
outlineListEl.innerHTML = "";
|
|
7474
|
+
for (const entry of outlineEntries) {
|
|
7475
|
+
const itemBtn = document.createElement("button");
|
|
7476
|
+
itemBtn.type = "button";
|
|
7477
|
+
itemBtn.className = "outline-entry";
|
|
7478
|
+
itemBtn.dataset.outlineId = String(entry.id || "");
|
|
7479
|
+
itemBtn.style.paddingLeft = (10 + Math.max(0, (entry.depth || 1) - 1) * 14) + "px";
|
|
7480
|
+
itemBtn.title = getOutlineKindLabel(entry.kind) + " · line " + String(entry.lineStart || 1) + "\n" + String(entry.label || "");
|
|
7481
|
+
|
|
7482
|
+
const kindEl = document.createElement("span");
|
|
7483
|
+
kindEl.className = "outline-entry-kind";
|
|
7484
|
+
kindEl.textContent = getOutlineKindBadge(entry.kind);
|
|
7485
|
+
itemBtn.appendChild(kindEl);
|
|
7486
|
+
|
|
7487
|
+
const titleEl = document.createElement("span");
|
|
7488
|
+
titleEl.className = "outline-entry-title";
|
|
7489
|
+
titleEl.textContent = String(entry.label || "");
|
|
7490
|
+
itemBtn.appendChild(titleEl);
|
|
7491
|
+
|
|
7492
|
+
const metaEl = document.createElement("span");
|
|
7493
|
+
metaEl.className = "outline-entry-meta";
|
|
7494
|
+
metaEl.textContent = "L" + String(entry.lineStart || 1);
|
|
7495
|
+
itemBtn.appendChild(metaEl);
|
|
7496
|
+
|
|
7497
|
+
outlineListEl.appendChild(itemBtn);
|
|
7498
|
+
}
|
|
7499
|
+
}
|
|
7500
|
+
|
|
7501
|
+
function buildOutlineEntryAnchor(entry) {
|
|
7502
|
+
if (!entry) return null;
|
|
7503
|
+
return normalizeReviewNote({
|
|
7504
|
+
selectionStart: entry.selectionStart,
|
|
7505
|
+
selectionEnd: entry.selectionEnd,
|
|
7506
|
+
lineStart: entry.lineStart,
|
|
7507
|
+
lineEnd: entry.lineEnd,
|
|
7508
|
+
selectedText: entry.selectedText,
|
|
7509
|
+
selectedDisplayText: entry.selectedDisplayText || entry.label,
|
|
7510
|
+
});
|
|
7511
|
+
}
|
|
7512
|
+
|
|
7513
|
+
function jumpToOutlineEntry(entryId) {
|
|
7514
|
+
const entry = outlineEntries.find((candidate) => candidate && String(candidate.id || "") === String(entryId || ""));
|
|
7515
|
+
if (!entry) return false;
|
|
7516
|
+
const anchor = buildOutlineEntryAnchor(entry);
|
|
7517
|
+
if (!anchor) return false;
|
|
7518
|
+
return jumpToReviewAnchor(anchor, {
|
|
7519
|
+
statusMessage: "Jumped to outline entry.",
|
|
7520
|
+
afterJump: () => {
|
|
7521
|
+
revealReviewNoteInPreview(anchor);
|
|
7522
|
+
},
|
|
7523
|
+
});
|
|
7524
|
+
}
|
|
7525
|
+
|
|
7526
|
+
function closeOutline(options) {
|
|
7527
|
+
if (!outlineOverlayEl || outlineOverlayEl.hidden) return;
|
|
7528
|
+
outlineOverlayEl.hidden = true;
|
|
7529
|
+
updateOutlineUi();
|
|
7530
|
+
if (editorView === "markdown") {
|
|
7531
|
+
scheduleEditorLineNumberRender();
|
|
7532
|
+
}
|
|
7533
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
7534
|
+
? options.focusTarget
|
|
7535
|
+
: (outlineReturnFocusEl || outlineBtn || sourceTextEl);
|
|
7536
|
+
outlineReturnFocusEl = null;
|
|
7537
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
7538
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
7539
|
+
? window.requestAnimationFrame.bind(window)
|
|
7540
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
7541
|
+
schedule(() => focusTarget.focus());
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
|
|
7545
|
+
function openOutline() {
|
|
7546
|
+
if (!outlineOverlayEl) return;
|
|
7547
|
+
if (isReviewNotesOpen()) {
|
|
7548
|
+
closeReviewNotes({ focusTarget: null });
|
|
7549
|
+
}
|
|
7550
|
+
outlineReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
7551
|
+
? document.activeElement
|
|
7552
|
+
: sourceTextEl;
|
|
7553
|
+
outlineOverlayEl.hidden = false;
|
|
7554
|
+
updateOutlineUi();
|
|
7555
|
+
if (editorView === "markdown") {
|
|
7556
|
+
scheduleEditorLineNumberRender();
|
|
7557
|
+
}
|
|
7558
|
+
}
|
|
7559
|
+
|
|
7560
|
+
function toggleOutline() {
|
|
7561
|
+
if (isOutlineOpen()) {
|
|
7562
|
+
closeOutline({ focusTarget: outlineBtn || sourceTextEl });
|
|
7563
|
+
} else {
|
|
7564
|
+
openOutline();
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7014
7568
|
function updateReviewNotesUi() {
|
|
7015
7569
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
7016
7570
|
const count = reviewNotes.length;
|
|
@@ -7382,6 +7936,18 @@
|
|
|
7382
7936
|
selection.removeAllRanges();
|
|
7383
7937
|
}
|
|
7384
7938
|
clearPreviewCommentSelection();
|
|
7939
|
+
const current = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
7940
|
+
const range = resolveReviewNoteRange(previewNote, current);
|
|
7941
|
+
if (range && sourceTextEl) {
|
|
7942
|
+
try {
|
|
7943
|
+
sourceTextEl.focus({ preventScroll: true });
|
|
7944
|
+
} catch {
|
|
7945
|
+
sourceTextEl.focus();
|
|
7946
|
+
}
|
|
7947
|
+
if (typeof sourceTextEl.setSelectionRange === "function") {
|
|
7948
|
+
sourceTextEl.setSelectionRange(range.start, range.end);
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7385
7951
|
});
|
|
7386
7952
|
},
|
|
7387
7953
|
});
|
|
@@ -7537,6 +8103,9 @@
|
|
|
7537
8103
|
if (isReviewNotesOpen()) {
|
|
7538
8104
|
closeReviewNotes({ focusTarget: null });
|
|
7539
8105
|
}
|
|
8106
|
+
if (isOutlineOpen()) {
|
|
8107
|
+
closeOutline({ focusTarget: null });
|
|
8108
|
+
}
|
|
7540
8109
|
scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
7541
8110
|
? document.activeElement
|
|
7542
8111
|
: sourceTextEl;
|
|
@@ -7580,6 +8149,9 @@
|
|
|
7580
8149
|
if (isScratchpadOpen()) {
|
|
7581
8150
|
closeScratchpad({ focusTarget: null });
|
|
7582
8151
|
}
|
|
8152
|
+
if (isOutlineOpen()) {
|
|
8153
|
+
closeOutline({ focusTarget: null });
|
|
8154
|
+
}
|
|
7583
8155
|
reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
7584
8156
|
? document.activeElement
|
|
7585
8157
|
: sourceTextEl;
|
|
@@ -7697,6 +8269,7 @@
|
|
|
7697
8269
|
if (editorView === "preview") {
|
|
7698
8270
|
scheduleSourcePreviewRender(0);
|
|
7699
8271
|
}
|
|
8272
|
+
updateOutlineUi();
|
|
7700
8273
|
}
|
|
7701
8274
|
|
|
7702
8275
|
function setEditorHighlightMode(mode) {
|
|
@@ -8896,6 +9469,7 @@
|
|
|
8896
9469
|
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
8897
9470
|
scheduleEditorMetaUpdate();
|
|
8898
9471
|
updateEditorSelectionCommentUi();
|
|
9472
|
+
updateOutlineUi();
|
|
8899
9473
|
if (isReviewNotesOpen() && reviewNotes.length > 0) {
|
|
8900
9474
|
renderReviewNotesList();
|
|
8901
9475
|
updateReviewNotesUi();
|
|
@@ -9284,6 +9858,35 @@
|
|
|
9284
9858
|
});
|
|
9285
9859
|
}
|
|
9286
9860
|
|
|
9861
|
+
if (outlineBtn) {
|
|
9862
|
+
outlineBtn.addEventListener("click", () => {
|
|
9863
|
+
toggleOutline();
|
|
9864
|
+
});
|
|
9865
|
+
}
|
|
9866
|
+
|
|
9867
|
+
if (outlineCloseBtn) {
|
|
9868
|
+
outlineCloseBtn.addEventListener("click", () => {
|
|
9869
|
+
closeOutline();
|
|
9870
|
+
});
|
|
9871
|
+
}
|
|
9872
|
+
|
|
9873
|
+
if (outlineDoneBtn) {
|
|
9874
|
+
outlineDoneBtn.addEventListener("click", () => {
|
|
9875
|
+
closeOutline();
|
|
9876
|
+
});
|
|
9877
|
+
}
|
|
9878
|
+
|
|
9879
|
+
if (outlineListEl) {
|
|
9880
|
+
outlineListEl.addEventListener("click", (event) => {
|
|
9881
|
+
const target = event.target;
|
|
9882
|
+
const entryBtn = target instanceof Element ? target.closest(".outline-entry") : null;
|
|
9883
|
+
if (!entryBtn) return;
|
|
9884
|
+
const outlineId = entryBtn.getAttribute("data-outline-id") || "";
|
|
9885
|
+
if (!outlineId) return;
|
|
9886
|
+
jumpToOutlineEntry(outlineId);
|
|
9887
|
+
});
|
|
9888
|
+
}
|
|
9889
|
+
|
|
9287
9890
|
if (reviewNotesCloseBtn) {
|
|
9288
9891
|
reviewNotesCloseBtn.addEventListener("click", () => {
|
|
9289
9892
|
closeReviewNotes();
|
package/client/studio.css
CHANGED
|
@@ -227,13 +227,15 @@
|
|
|
227
227
|
|
|
228
228
|
#scratchpadBtn.has-content,
|
|
229
229
|
#reviewNotesBtn.has-content,
|
|
230
|
-
#reviewNotesBtn.is-active
|
|
230
|
+
#reviewNotesBtn.is-active,
|
|
231
|
+
#outlineBtn.is-active {
|
|
231
232
|
border-color: var(--accent);
|
|
232
233
|
color: var(--accent);
|
|
233
234
|
font-weight: 600;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
#reviewNotesBtn.is-active
|
|
237
|
+
#reviewNotesBtn.is-active,
|
|
238
|
+
#outlineBtn.is-active {
|
|
237
239
|
background: var(--accent-soft);
|
|
238
240
|
}
|
|
239
241
|
|
|
@@ -336,7 +338,8 @@
|
|
|
336
338
|
gap: 8px;
|
|
337
339
|
}
|
|
338
340
|
|
|
339
|
-
.review-notes-dock-wrap
|
|
341
|
+
.review-notes-dock-wrap,
|
|
342
|
+
.outline-dock-wrap {
|
|
340
343
|
flex: 0 0 clamp(300px, 34%, 420px);
|
|
341
344
|
min-width: 280px;
|
|
342
345
|
min-height: 0;
|
|
@@ -348,11 +351,13 @@
|
|
|
348
351
|
overflow: hidden;
|
|
349
352
|
}
|
|
350
353
|
|
|
351
|
-
.review-notes-dock-wrap[hidden]
|
|
354
|
+
.review-notes-dock-wrap[hidden],
|
|
355
|
+
.outline-dock-wrap[hidden] {
|
|
352
356
|
display: none !important;
|
|
353
357
|
}
|
|
354
358
|
|
|
355
|
-
.review-notes-dock
|
|
359
|
+
.review-notes-dock,
|
|
360
|
+
.outline-dock {
|
|
356
361
|
width: 100%;
|
|
357
362
|
min-height: 0;
|
|
358
363
|
display: flex;
|
|
@@ -1824,15 +1829,18 @@
|
|
|
1824
1829
|
filter: brightness(0.95);
|
|
1825
1830
|
}
|
|
1826
1831
|
|
|
1827
|
-
.review-notes-dock .scratchpad-header
|
|
1832
|
+
.review-notes-dock .scratchpad-header,
|
|
1833
|
+
.outline-dock .scratchpad-header {
|
|
1828
1834
|
padding: 12px 14px 10px;
|
|
1829
1835
|
}
|
|
1830
1836
|
|
|
1831
|
-
.review-notes-dock .scratchpad-header h2
|
|
1837
|
+
.review-notes-dock .scratchpad-header h2,
|
|
1838
|
+
.outline-dock .scratchpad-header h2 {
|
|
1832
1839
|
font-size: 15px;
|
|
1833
1840
|
}
|
|
1834
1841
|
|
|
1835
|
-
.review-notes-dock .scratchpad-description
|
|
1842
|
+
.review-notes-dock .scratchpad-description,
|
|
1843
|
+
.outline-dock .scratchpad-description {
|
|
1836
1844
|
font-size: 11px;
|
|
1837
1845
|
line-height: 1.4;
|
|
1838
1846
|
word-break: normal;
|
|
@@ -1895,6 +1903,60 @@
|
|
|
1895
1903
|
justify-content: space-between;
|
|
1896
1904
|
}
|
|
1897
1905
|
|
|
1906
|
+
.outline-list {
|
|
1907
|
+
display: flex;
|
|
1908
|
+
flex: 1 1 auto;
|
|
1909
|
+
min-height: 0;
|
|
1910
|
+
flex-direction: column;
|
|
1911
|
+
gap: 6px;
|
|
1912
|
+
padding: 14px;
|
|
1913
|
+
overflow: auto;
|
|
1914
|
+
background: var(--panel);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.outline-entry {
|
|
1918
|
+
width: 100%;
|
|
1919
|
+
display: grid;
|
|
1920
|
+
grid-template-columns: auto 1fr auto;
|
|
1921
|
+
align-items: baseline;
|
|
1922
|
+
gap: 8px;
|
|
1923
|
+
text-align: left;
|
|
1924
|
+
border-radius: 10px;
|
|
1925
|
+
padding: 8px 10px;
|
|
1926
|
+
background: var(--panel-2);
|
|
1927
|
+
border: 1px solid var(--border-muted);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
.outline-entry:hover,
|
|
1931
|
+
.outline-entry:focus-visible {
|
|
1932
|
+
border-color: var(--accent);
|
|
1933
|
+
background: var(--accent-soft);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
.outline-entry-kind {
|
|
1937
|
+
font-size: 11px;
|
|
1938
|
+
font-weight: 700;
|
|
1939
|
+
letter-spacing: 0.03em;
|
|
1940
|
+
text-transform: uppercase;
|
|
1941
|
+
color: var(--accent);
|
|
1942
|
+
white-space: nowrap;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
.outline-entry-title {
|
|
1946
|
+
min-width: 0;
|
|
1947
|
+
font-size: 13px;
|
|
1948
|
+
line-height: 1.35;
|
|
1949
|
+
color: var(--text);
|
|
1950
|
+
overflow-wrap: anywhere;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
.outline-entry-meta {
|
|
1954
|
+
font-size: 11px;
|
|
1955
|
+
color: var(--muted);
|
|
1956
|
+
white-space: nowrap;
|
|
1957
|
+
font-variant-numeric: tabular-nums;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1898
1960
|
.review-note-card {
|
|
1899
1961
|
border: 1px solid var(--border-muted);
|
|
1900
1962
|
border-radius: 12px;
|
|
@@ -2002,7 +2064,8 @@
|
|
|
2002
2064
|
flex-direction: column;
|
|
2003
2065
|
}
|
|
2004
2066
|
|
|
2005
|
-
.review-notes-dock-wrap
|
|
2067
|
+
.review-notes-dock-wrap,
|
|
2068
|
+
.outline-dock-wrap {
|
|
2006
2069
|
flex: 0 0 auto;
|
|
2007
2070
|
min-width: 0;
|
|
2008
2071
|
max-height: min(42vh, 420px);
|
package/index.ts
CHANGED
|
@@ -6010,6 +6010,7 @@ ${cssVarsBlock}
|
|
|
6010
6010
|
<div class="section-header-actions">
|
|
6011
6011
|
<button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
6012
6012
|
<button id="reviewNotesBtn" type="button" title="Toggle local comments beside the current editor document or draft. Comments stay outside the document text and can later be converted into [an: ...] annotations.">Comments</button>
|
|
6013
|
+
<button id="outlineBtn" type="button" title="Toggle document outline for the current editor text. Outline entries can jump between raw editor and preview.">Outline</button>
|
|
6013
6014
|
<button id="scratchpadBtn" type="button" title="Open a local persistent scratchpad for the current editor document or draft. Scratchpad text is never run, critiqued, or exported unless you explicitly insert it into the editor.">Scratchpad</button>
|
|
6014
6015
|
</div>
|
|
6015
6016
|
</div>
|
|
@@ -6102,6 +6103,27 @@ ${cssVarsBlock}
|
|
|
6102
6103
|
</div>
|
|
6103
6104
|
<div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
|
|
6104
6105
|
</div>
|
|
6106
|
+
<aside id="outlineOverlay" class="outline-dock-wrap" hidden>
|
|
6107
|
+
<div id="outlineDialog" class="outline-dock" role="complementary" aria-labelledby="outlineTitle">
|
|
6108
|
+
<div class="scratchpad-header">
|
|
6109
|
+
<div>
|
|
6110
|
+
<h2 id="outlineTitle">Outline</h2>
|
|
6111
|
+
<p class="scratchpad-description">Document structure for the current editor text. Click an entry to jump in the raw editor and, when available, reveal the matching preview location.</p>
|
|
6112
|
+
</div>
|
|
6113
|
+
<button id="outlineCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide outline" title="Hide outline">✕</button>
|
|
6114
|
+
</div>
|
|
6115
|
+
<div class="review-notes-toolbar">
|
|
6116
|
+
<span id="outlineMeta" class="scratchpad-meta">No outline entries</span>
|
|
6117
|
+
</div>
|
|
6118
|
+
<div id="outlineEmptyState" class="review-notes-empty">No outline available yet for this document or syntax mode.</div>
|
|
6119
|
+
<div id="outlineList" class="outline-list" aria-live="polite"></div>
|
|
6120
|
+
<div class="review-notes-dock-footer">
|
|
6121
|
+
<div class="scratchpad-actions">
|
|
6122
|
+
<button id="outlineDoneBtn" type="button" title="Hide the outline rail.">Hide</button>
|
|
6123
|
+
</div>
|
|
6124
|
+
</div>
|
|
6125
|
+
</div>
|
|
6126
|
+
</aside>
|
|
6105
6127
|
<aside id="reviewNotesOverlay" class="review-notes-dock-wrap" hidden>
|
|
6106
6128
|
<div id="reviewNotesDialog" class="review-notes-dock" role="complementary" aria-labelledby="reviewNotesTitle">
|
|
6107
6129
|
<div class="scratchpad-header">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.52",
|
|
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",
|