md-redline 0.1.0
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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/md-redline +255 -0
- package/bin/test-windows.ps1 +70 -0
- package/dist/assets/_baseFor-Ck08IaSF.js +1 -0
- package/dist/assets/arc-DI2g9LXK.js +1 -0
- package/dist/assets/architecture-YZFGNWBL-BDgMfc-b.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-Dg1hcUEa.js +36 -0
- package/dist/assets/array-DOVTz2Mq.js +1 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BAXkTCAk.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-BIkgwQSx.js +10 -0
- package/dist/assets/channel-DPCihw7y.js +1 -0
- package/dist/assets/chunk-2KRD3SAO-Dc_tBGsw.js +1 -0
- package/dist/assets/chunk-336JU56O-Dhi-ID9Y.js +2 -0
- package/dist/assets/chunk-426QAEUC-DnFdrNMW.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Z63FkGov.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-BAiBlfyy.js +206 -0
- package/dist/assets/chunk-55IACEB6-BXDWXbxy.js +1 -0
- package/dist/assets/chunk-5FUZZQ4R-C72e1c_O.js +62 -0
- package/dist/assets/chunk-5PVQY5BW-BBHW_uCu.js +2 -0
- package/dist/assets/chunk-67CJDMHE-3Cf_D9m6.js +1 -0
- package/dist/assets/chunk-7N4EOEYR-DAXUXJ2c.js +1 -0
- package/dist/assets/chunk-AA7GKIK3-Dr7fOryc.js +1 -0
- package/dist/assets/chunk-BSJP7CBP-BmsSs1Nt.js +1 -0
- package/dist/assets/chunk-CIAEETIT-QDzV-X_Y.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-C25WFHxY.js +1 -0
- package/dist/assets/chunk-ENJZ2VHE-_OzxcZOU.js +10 -0
- package/dist/assets/chunk-FMBD7UC4-CjsTKY4u.js +15 -0
- package/dist/assets/chunk-FOC6F5B3-g-xaH5nc.js +1 -0
- package/dist/assets/chunk-ICPOFSXX-iKiUSjDK.js +121 -0
- package/dist/assets/chunk-K5T4RW27-CKR-lPBN.js +94 -0
- package/dist/assets/chunk-KGLVRYIC-DRccT-B_.js +1 -0
- package/dist/assets/chunk-LIHQZDEY-DTbMwMXj.js +1 -0
- package/dist/assets/chunk-ORNJ4GCN-DlerdcWX.js +1 -0
- package/dist/assets/chunk-OYMX7WX6-Dekv1on2.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-BHu0RdKl.js +1 -0
- package/dist/assets/chunk-U2HBQHQK-BvtlVHAg.js +70 -0
- package/dist/assets/chunk-X2U36JSP-BI_g8mub.js +1 -0
- package/dist/assets/chunk-XPW4576I-B39JkmSE.js +32 -0
- package/dist/assets/chunk-YZCP3GAM-BfPcXRm2.js +1 -0
- package/dist/assets/chunk-ZZ45TVLE-Bg4q68wZ.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-p73p727_.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-C4Ftpivp.js +1 -0
- package/dist/assets/clone-CI9aUwHe.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-7BpAeDh5.js +1 -0
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +321 -0
- package/dist/assets/dagre-CilMRazv.js +1 -0
- package/dist/assets/dagre-KV5264BT-DDMqpjkB.js +4 -0
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-BFeyfnCx.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-DoqT-PtF.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-BPV6KADk.js +43 -0
- package/dist/assets/diagram-TYMM5635-okvcTBtl.js +24 -0
- package/dist/assets/dist-C_eddq6m.js +1 -0
- package/dist/assets/erDiagram-SMLLAGMA-Dl-Ixy8n.js +85 -0
- package/dist/assets/flatten-B8XIuT0x.js +1 -0
- package/dist/assets/flowDiagram-DWJPFMVM-CsqWAx5r.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-mIt6zVeF.js +292 -0
- package/dist/assets/gitGraph-7Q5UKJZL-COXHGMvj.js +1 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-syVqZJX_.js +106 -0
- package/dist/assets/graphlib-Bpd0q3yO.js +1 -0
- package/dist/assets/index-BoggyWS0.css +2 -0
- package/dist/assets/index-aLvjHQW4.js +104 -0
- package/dist/assets/info-OMHHGYJF-B-0wfxwL.js +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-C0_uqsVa.js +2 -0
- package/dist/assets/init-Bft5Ffpj.js +1 -0
- package/dist/assets/isEmpty-BrFi5AqV.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-CTjFbDBV.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-BDBcej1q.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-Ylgzakw7.js +89 -0
- package/dist/assets/katex-Uj9wLT16.js +265 -0
- package/dist/assets/line-CRxEwpOv.js +1 -0
- package/dist/assets/linear-PDPfFByd.js +1 -0
- package/dist/assets/mermaid-parser.core-CY-XNOOy.js +4 -0
- package/dist/assets/mermaid.core-BPlTADIX.js +11 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-TefzJnBM.js +96 -0
- package/dist/assets/ordinal-DIg8h6NI.js +1 -0
- package/dist/assets/packet-4T2RLAQJ-BW1T_A-C.js +1 -0
- package/dist/assets/path-DfRbCp9y.js +1 -0
- package/dist/assets/pie-ZZUOXDRM-DkKU-SFu.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-BCXuaeEy.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-VSBAicWL.js +7 -0
- package/dist/assets/radar-PYXPWWZC-CYvTacKJ.js +1 -0
- package/dist/assets/reduce-CV2X8n1a.js +1 -0
- package/dist/assets/requirementDiagram-MS252O5E-4NeL9Z6J.js +84 -0
- package/dist/assets/rough.esm-Bbn_-PMU.js +1 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-DMBSDnrH.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DVpzcZUi.js +157 -0
- package/dist/assets/src-PKe5NtkK.js +1 -0
- package/dist/assets/stateDiagram-FHFEXIEX-BkHTlCjL.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-nMeWu9fP.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-CyLt92nf.js +120 -0
- package/dist/assets/treeView-SZITEDCU-BUgcJ4eR.js +1 -0
- package/dist/assets/treemap-W4RFUUIX-BIWGQ4Pw.js +1 -0
- package/dist/assets/vennDiagram-DHZGUBPP-BCK0xB_m.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DMZZRlby.js +1 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BisBgfsF.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-D_REDciv.js +7 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +14 -0
- package/dist/screenshot.png +0 -0
- package/index.html +13 -0
- package/package.json +105 -0
- package/public/favicon.svg +15 -0
- package/public/screenshot.png +0 -0
- package/server/index.test.ts +814 -0
- package/server/index.ts +736 -0
- package/server/preferences.test.ts +126 -0
- package/server/preferences.ts +76 -0
- package/src/App.tsx +1620 -0
- package/src/components/ActionButton.tsx +41 -0
- package/src/components/CommandPalette.tsx +191 -0
- package/src/components/CommentCard.tsx +556 -0
- package/src/components/CommentForm.tsx +285 -0
- package/src/components/CommentSidebar.tsx +428 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/ContextMenu.tsx +220 -0
- package/src/components/DragHandles.tsx +48 -0
- package/src/components/FileExplorer.tsx +251 -0
- package/src/components/FileOpener.tsx +304 -0
- package/src/components/IconButton.tsx +32 -0
- package/src/components/KeyboardShortcutsPanel.tsx +136 -0
- package/src/components/MarkdownViewer.tsx +682 -0
- package/src/components/RawView.tsx +798 -0
- package/src/components/SearchBar.tsx +129 -0
- package/src/components/Separator.tsx +7 -0
- package/src/components/SettingsPanel.tsx +813 -0
- package/src/components/SplitIconButton.tsx +133 -0
- package/src/components/TabBar.tsx +594 -0
- package/src/components/TableOfContents.tsx +70 -0
- package/src/components/ThemeSelector.tsx +159 -0
- package/src/components/Toast.tsx +99 -0
- package/src/components/Toolbar.tsx +161 -0
- package/src/components/iconButtonVariants.ts +19 -0
- package/src/components/rawView.test.ts +291 -0
- package/src/contexts/SettingsContext.tsx +120 -0
- package/src/hooks/useAuthor.test.ts +58 -0
- package/src/hooks/useAuthor.ts +69 -0
- package/src/hooks/useAutoResize.ts +20 -0
- package/src/hooks/useCommentCardTriggers.ts +20 -0
- package/src/hooks/useComments.test.ts +773 -0
- package/src/hooks/useComments.ts +332 -0
- package/src/hooks/useContextMenu.ts +48 -0
- package/src/hooks/useContextMenuItems.ts +392 -0
- package/src/hooks/useDiffSnapshot.test.ts +130 -0
- package/src/hooks/useDiffSnapshot.ts +67 -0
- package/src/hooks/useDragHandles.ts +417 -0
- package/src/hooks/useFileWatcher.ts +45 -0
- package/src/hooks/useHeadingTracking.ts +84 -0
- package/src/hooks/useMermaidRenderer.ts +75 -0
- package/src/hooks/useModalState.ts +22 -0
- package/src/hooks/usePageVisible.test.ts +69 -0
- package/src/hooks/usePageVisible.ts +19 -0
- package/src/hooks/usePaneLayout.test.ts +108 -0
- package/src/hooks/usePaneLayout.ts +102 -0
- package/src/hooks/useRecentFiles.test.ts +103 -0
- package/src/hooks/useRecentFiles.ts +99 -0
- package/src/hooks/useResizablePanel.test.ts +84 -0
- package/src/hooks/useResizablePanel.ts +118 -0
- package/src/hooks/useSearch.test.ts +72 -0
- package/src/hooks/useSearch.ts +53 -0
- package/src/hooks/useSelection.ts +48 -0
- package/src/hooks/useSessionPersistence.test.ts +59 -0
- package/src/hooks/useSessionPersistence.ts +43 -0
- package/src/hooks/useTabs.test.ts +127 -0
- package/src/hooks/useTabs.ts +561 -0
- package/src/hooks/useThemePersistence.ts +41 -0
- package/src/hooks/useToast.ts +27 -0
- package/src/index.css +1047 -0
- package/src/lib/agent-prompts.test.ts +34 -0
- package/src/lib/agent-prompts.ts +68 -0
- package/src/lib/comment-editor-state.ts +6 -0
- package/src/lib/comment-parser.test.ts +1959 -0
- package/src/lib/comment-parser.ts +1021 -0
- package/src/lib/diff.test.ts +164 -0
- package/src/lib/diff.ts +139 -0
- package/src/lib/heading-slugs.test.ts +85 -0
- package/src/lib/heading-slugs.ts +44 -0
- package/src/lib/http.test.ts +43 -0
- package/src/lib/http.ts +29 -0
- package/src/lib/mermaid-highlights.test.ts +517 -0
- package/src/lib/mermaid-highlights.ts +936 -0
- package/src/lib/mermaid-renderer.test.ts +114 -0
- package/src/lib/mermaid-renderer.ts +89 -0
- package/src/lib/path-utils.test.ts +17 -0
- package/src/lib/path-utils.ts +7 -0
- package/src/lib/platform.test.ts +58 -0
- package/src/lib/platform.ts +14 -0
- package/src/lib/preferences-client.test.ts +177 -0
- package/src/lib/preferences-client.ts +94 -0
- package/src/lib/selection-resolver.test.ts +118 -0
- package/src/lib/selection-resolver.ts +37 -0
- package/src/lib/settings.test.ts +152 -0
- package/src/lib/settings.ts +78 -0
- package/src/lib/shortcut-label.tsx +18 -0
- package/src/lib/themes.ts +21 -0
- package/src/lib/visible-text.test.ts +86 -0
- package/src/lib/visible-text.ts +77 -0
- package/src/main.tsx +22 -0
- package/src/markdown/pipeline.test.ts +82 -0
- package/src/markdown/pipeline.ts +33 -0
- package/src/types.test.ts +43 -0
- package/src/types.ts +46 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { unified } from 'unified';
|
|
12
|
+
import remarkParse from 'remark-parse';
|
|
13
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
14
|
+
import remarkGfm from 'remark-gfm';
|
|
15
|
+
import { highlightSearchMatches } from './MarkdownViewer';
|
|
16
|
+
import { COMMENT_MARKER_RE, parseComments } from '../lib/comment-parser';
|
|
17
|
+
import { uniqueSlugs } from '../lib/heading-slugs';
|
|
18
|
+
import { computeDiff, type DiffLine } from '../lib/diff';
|
|
19
|
+
import { SplitIconButton } from './SplitIconButton';
|
|
20
|
+
|
|
21
|
+
// Markdown syntax highlighting patterns (order matters — first match wins per region)
|
|
22
|
+
interface SyntaxRule {
|
|
23
|
+
pattern: RegExp;
|
|
24
|
+
className: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SYNTAX_RULES: SyntaxRule[] = [
|
|
28
|
+
// Comment markers — highest priority
|
|
29
|
+
{ pattern: COMMENT_MARKER_RE, className: 'raw-comment-marker' },
|
|
30
|
+
// Fenced code blocks (``` or ~~~)
|
|
31
|
+
{ pattern: /^(`{3,}|~{3,}).*$(?:\n[\s\S]*?)?^(\1)/gm, className: 'raw-code-block' },
|
|
32
|
+
// Inline code
|
|
33
|
+
{ pattern: /`[^`\n]+`/g, className: 'raw-inline-code' },
|
|
34
|
+
// Headings
|
|
35
|
+
{ pattern: /^#{1,6}\s.*$/gm, className: 'raw-heading' },
|
|
36
|
+
// Bold
|
|
37
|
+
{ pattern: /\*\*[^*]+\*\*/g, className: 'raw-bold' },
|
|
38
|
+
// Italic (but not inside bold)
|
|
39
|
+
{ pattern: /(?<!\*)\*[^*\n]+\*(?!\*)/g, className: 'raw-italic' },
|
|
40
|
+
// Links [text](url)
|
|
41
|
+
{ pattern: /\[([^\]]+)\]\([^)]+\)/g, className: 'raw-link' },
|
|
42
|
+
// Blockquotes
|
|
43
|
+
{ pattern: /^>\s.*$/gm, className: 'raw-blockquote' },
|
|
44
|
+
// List markers
|
|
45
|
+
{ pattern: /^(\s*)([-*+]|\d+\.)\s/gm, className: 'raw-list-marker' },
|
|
46
|
+
// Horizontal rules (3+ of the same character)
|
|
47
|
+
{ pattern: /^(-{3,}|\*{3,}|_{3,})\s*$/gm, className: 'raw-hr' },
|
|
48
|
+
// Table pipes
|
|
49
|
+
{ pattern: /^\|.*\|$/gm, className: 'raw-table' },
|
|
50
|
+
// Frontmatter — no `m` flag so ^ only matches start of string
|
|
51
|
+
{ pattern: /^---\n[\s\S]*?\n---/g, className: 'raw-frontmatter' },
|
|
52
|
+
// HTML comments (non-@comment ones)
|
|
53
|
+
{ pattern: /<!--(?! @comment)[\s\S]*?-->/g, className: 'raw-html-comment' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export interface RawViewHandle {
|
|
57
|
+
scrollToComment: (commentId: string) => void;
|
|
58
|
+
scrollToHeading: (headingId: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Props {
|
|
62
|
+
rawMarkdown: string;
|
|
63
|
+
searchQuery?: string;
|
|
64
|
+
searchActiveIndex?: number;
|
|
65
|
+
onSearchCount?: (count: number) => void;
|
|
66
|
+
activeCommentId: string | null;
|
|
67
|
+
diffSnapshot?: string | null;
|
|
68
|
+
diffEnabled?: boolean;
|
|
69
|
+
onDiffToggle?: () => void;
|
|
70
|
+
onClearSnapshot?: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface DisplayRow {
|
|
74
|
+
type: 'same' | 'added' | 'removed';
|
|
75
|
+
html: string;
|
|
76
|
+
lineNo: number | undefined;
|
|
77
|
+
sourceLineIndex?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Region = { start: number; end: number; className: string; id?: string };
|
|
81
|
+
type MarkdownAstNode = {
|
|
82
|
+
type: string;
|
|
83
|
+
depth?: number;
|
|
84
|
+
value?: string;
|
|
85
|
+
alt?: string;
|
|
86
|
+
children?: MarkdownAstNode[];
|
|
87
|
+
position?: {
|
|
88
|
+
start?: {
|
|
89
|
+
line?: number;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export interface RawHeading {
|
|
95
|
+
id: string;
|
|
96
|
+
text: string;
|
|
97
|
+
level: number;
|
|
98
|
+
lineIndex: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rawHeadingProcessor = unified()
|
|
102
|
+
.use(remarkParse)
|
|
103
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
104
|
+
.use(remarkGfm);
|
|
105
|
+
|
|
106
|
+
function extractNodeText(node: MarkdownAstNode): string {
|
|
107
|
+
if (node.type === 'text' || node.type === 'inlineCode' || node.type === 'html') {
|
|
108
|
+
return node.value ?? '';
|
|
109
|
+
}
|
|
110
|
+
if (node.type === 'image') {
|
|
111
|
+
return node.alt ?? '';
|
|
112
|
+
}
|
|
113
|
+
if (node.type === 'break') {
|
|
114
|
+
return ' ';
|
|
115
|
+
}
|
|
116
|
+
return node.children?.map(extractNodeText).join('') ?? '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function extractRawHeadings(rawMarkdown: string): RawHeading[] {
|
|
120
|
+
COMMENT_MARKER_RE.lastIndex = 0;
|
|
121
|
+
const cleanRaw = rawMarkdown.replace(COMMENT_MARKER_RE, '');
|
|
122
|
+
const tree = rawHeadingProcessor.parse(cleanRaw) as MarkdownAstNode;
|
|
123
|
+
const headings: Array<{ text: string; level: number; lineIndex: number }> = [];
|
|
124
|
+
|
|
125
|
+
const visit = (node: MarkdownAstNode) => {
|
|
126
|
+
if (node.type === 'heading') {
|
|
127
|
+
const line = node.position?.start?.line;
|
|
128
|
+
if (line != null) {
|
|
129
|
+
headings.push({
|
|
130
|
+
text: extractNodeText(node).trim(),
|
|
131
|
+
level: node.depth ?? 1,
|
|
132
|
+
lineIndex: Math.max(0, line - 1),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
node.children?.forEach(visit);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
visit(tree);
|
|
140
|
+
|
|
141
|
+
const ids = uniqueSlugs(headings.map((heading) => heading.text));
|
|
142
|
+
return headings.map((heading, index) => ({
|
|
143
|
+
...heading,
|
|
144
|
+
id: ids[index],
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build highlighted HTML from raw markdown text.
|
|
150
|
+
* Returns the full HTML string with <span> wrappers for syntax highlighting.
|
|
151
|
+
*/
|
|
152
|
+
export function buildHighlightedHtml(raw: string): string {
|
|
153
|
+
// Step 1: Collect comment marker matches first (they have absolute priority)
|
|
154
|
+
const commentRegions: Region[] = [];
|
|
155
|
+
COMMENT_MARKER_RE.lastIndex = 0;
|
|
156
|
+
let cm: RegExpExecArray | null;
|
|
157
|
+
while ((cm = COMMENT_MARKER_RE.exec(raw)) !== null) {
|
|
158
|
+
const region: Region = {
|
|
159
|
+
start: cm.index,
|
|
160
|
+
end: cm.index + cm[0].length,
|
|
161
|
+
className: 'raw-comment-marker',
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
const jsonStr = cm[0].replace(/^<!-- @comment/, '').replace(/ -->$/, '');
|
|
165
|
+
const parsed = JSON.parse(jsonStr);
|
|
166
|
+
if (parsed.id) region.id = parsed.id;
|
|
167
|
+
} catch {
|
|
168
|
+
/* ignore parse errors */
|
|
169
|
+
}
|
|
170
|
+
commentRegions.push(region);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Step 2: Collect other syntax matches, skipping any that overlap comment markers
|
|
174
|
+
const otherRegions: Region[] = [];
|
|
175
|
+
for (const rule of SYNTAX_RULES) {
|
|
176
|
+
if (rule.className === 'raw-comment-marker') continue;
|
|
177
|
+
rule.pattern.lastIndex = 0;
|
|
178
|
+
let m: RegExpExecArray | null;
|
|
179
|
+
while ((m = rule.pattern.exec(raw)) !== null) {
|
|
180
|
+
const start = m.index;
|
|
181
|
+
const end = m.index + m[0].length;
|
|
182
|
+
// Skip if this region overlaps any comment marker
|
|
183
|
+
const overlapsComment = commentRegions.some((c) => start < c.end && end > c.start);
|
|
184
|
+
if (!overlapsComment) {
|
|
185
|
+
otherRegions.push({ start, end, className: rule.className });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 3: Merge and sort all regions
|
|
191
|
+
const allRegions = [...commentRegions, ...otherRegions];
|
|
192
|
+
allRegions.sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
|
|
193
|
+
|
|
194
|
+
// Step 4: Remove overlapping regions among non-comment rules (first match wins)
|
|
195
|
+
const filtered: Region[] = [];
|
|
196
|
+
let lastEnd = 0;
|
|
197
|
+
for (const r of allRegions) {
|
|
198
|
+
if (r.start >= lastEnd) {
|
|
199
|
+
filtered.push(r);
|
|
200
|
+
lastEnd = r.end;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Build HTML string
|
|
205
|
+
const parts: string[] = [];
|
|
206
|
+
let cursor = 0;
|
|
207
|
+
|
|
208
|
+
for (const r of filtered) {
|
|
209
|
+
if (r.start > cursor) {
|
|
210
|
+
parts.push(escapeHtml(raw.slice(cursor, r.start)));
|
|
211
|
+
}
|
|
212
|
+
const idAttr = r.id ? ` data-comment-id="${escapeAttr(r.id)}"` : '';
|
|
213
|
+
parts.push(
|
|
214
|
+
`<span class="${r.className}"${idAttr}>${escapeHtml(raw.slice(r.start, r.end))}</span>`,
|
|
215
|
+
);
|
|
216
|
+
cursor = r.end;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (cursor < raw.length) {
|
|
220
|
+
parts.push(escapeHtml(raw.slice(cursor)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return parts.join('');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Split highlighted HTML into per-line segments matching the source line count.
|
|
228
|
+
* Handles spans that cross line boundaries by closing/reopening tags.
|
|
229
|
+
*/
|
|
230
|
+
export function splitHighlightedHtml(raw: string, fullHtml: string): string[] {
|
|
231
|
+
const lines = raw.split('\n');
|
|
232
|
+
const result: string[] = [];
|
|
233
|
+
let current = '';
|
|
234
|
+
const openTags: string[] = [];
|
|
235
|
+
let i = 0;
|
|
236
|
+
|
|
237
|
+
while (i < fullHtml.length) {
|
|
238
|
+
if (fullHtml[i] === '\n') {
|
|
239
|
+
for (let t = openTags.length - 1; t >= 0; t--) {
|
|
240
|
+
current += '</span>';
|
|
241
|
+
}
|
|
242
|
+
result.push(current);
|
|
243
|
+
current = '';
|
|
244
|
+
for (const tag of openTags) {
|
|
245
|
+
current += tag;
|
|
246
|
+
}
|
|
247
|
+
i++;
|
|
248
|
+
} else if (fullHtml[i] === '<') {
|
|
249
|
+
const closeMatch = fullHtml.slice(i).match(/^<\/span>/);
|
|
250
|
+
if (closeMatch) {
|
|
251
|
+
current += closeMatch[0];
|
|
252
|
+
openTags.pop();
|
|
253
|
+
i += closeMatch[0].length;
|
|
254
|
+
} else {
|
|
255
|
+
const openMatch = fullHtml.slice(i).match(/^<span[^>]*>/);
|
|
256
|
+
if (openMatch) {
|
|
257
|
+
current += openMatch[0];
|
|
258
|
+
openTags.push(openMatch[0]);
|
|
259
|
+
i += openMatch[0].length;
|
|
260
|
+
} else {
|
|
261
|
+
current += fullHtml[i];
|
|
262
|
+
i++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
current += fullHtml[i];
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (let t = openTags.length - 1; t >= 0; t--) {
|
|
271
|
+
current += '</span>';
|
|
272
|
+
}
|
|
273
|
+
result.push(current);
|
|
274
|
+
|
|
275
|
+
while (result.length < lines.length) result.push('');
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function escapeHtml(s: string): string {
|
|
280
|
+
return s
|
|
281
|
+
.replace(/&/g, '&')
|
|
282
|
+
.replace(/</g, '<')
|
|
283
|
+
.replace(/>/g, '>')
|
|
284
|
+
.replace(/"/g, '"');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function escapeAttr(s: string): string {
|
|
288
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const RawView = forwardRef<RawViewHandle, Props>(function RawView(
|
|
292
|
+
{
|
|
293
|
+
rawMarkdown,
|
|
294
|
+
searchQuery,
|
|
295
|
+
searchActiveIndex,
|
|
296
|
+
onSearchCount,
|
|
297
|
+
activeCommentId,
|
|
298
|
+
diffSnapshot,
|
|
299
|
+
diffEnabled,
|
|
300
|
+
onDiffToggle,
|
|
301
|
+
onClearSnapshot,
|
|
302
|
+
},
|
|
303
|
+
ref,
|
|
304
|
+
) {
|
|
305
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
306
|
+
const tableRef = useRef<HTMLDivElement>(null);
|
|
307
|
+
const flashTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
308
|
+
const [copyFeedback, setCopyFeedback] = useState(false);
|
|
309
|
+
const [showComments, setShowComments] = useState(true);
|
|
310
|
+
const [activeDiffChunk, setActiveDiffChunk] = useState(0);
|
|
311
|
+
|
|
312
|
+
// Hide comment markers when entering diff mode so user sees only diffs
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (diffEnabled) setShowComments(false);
|
|
315
|
+
}, [diffEnabled]);
|
|
316
|
+
|
|
317
|
+
// Clean up flash timer on unmount
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
return () => {
|
|
320
|
+
if (flashTimerRef.current) clearTimeout(flashTimerRef.current);
|
|
321
|
+
};
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
const highlightedHtml = useMemo(() => buildHighlightedHtml(rawMarkdown), [rawMarkdown]);
|
|
325
|
+
const rawHeadings = useMemo(() => extractRawHeadings(rawMarkdown), [rawMarkdown]);
|
|
326
|
+
const headingIdsByLine = useMemo(
|
|
327
|
+
() => new Map(rawHeadings.map((heading) => [heading.lineIndex, heading.id])),
|
|
328
|
+
[rawHeadings],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const lineHtmls = useMemo(
|
|
332
|
+
() => splitHighlightedHtml(rawMarkdown, highlightedHtml),
|
|
333
|
+
[rawMarkdown, highlightedHtml],
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Diff computation — compare clean markdown (without comment markers) so that
|
|
337
|
+
// marker additions/removals don't appear as content changes
|
|
338
|
+
const diffLines = useMemo<DiffLine[] | null>(() => {
|
|
339
|
+
if (!diffEnabled || !diffSnapshot) return null;
|
|
340
|
+
const { cleanMarkdown: oldClean } = parseComments(diffSnapshot);
|
|
341
|
+
const { cleanMarkdown: newClean } = parseComments(rawMarkdown);
|
|
342
|
+
return computeDiff(oldClean, newClean);
|
|
343
|
+
}, [diffEnabled, diffSnapshot, rawMarkdown]);
|
|
344
|
+
|
|
345
|
+
const oldHighlightedHtml = useMemo(
|
|
346
|
+
() => (diffEnabled && diffSnapshot ? buildHighlightedHtml(diffSnapshot) : ''),
|
|
347
|
+
[diffEnabled, diffSnapshot],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const oldLineHtmls = useMemo(() => {
|
|
351
|
+
if (!diffEnabled || !diffSnapshot) return [];
|
|
352
|
+
return splitHighlightedHtml(diffSnapshot, oldHighlightedHtml);
|
|
353
|
+
}, [diffEnabled, diffSnapshot, oldHighlightedHtml]);
|
|
354
|
+
|
|
355
|
+
const displayRows = useMemo<DisplayRow[]>(() => {
|
|
356
|
+
if (!diffLines) {
|
|
357
|
+
return lineHtmls.map((html, i) => ({
|
|
358
|
+
type: 'same' as const,
|
|
359
|
+
html,
|
|
360
|
+
lineNo: i + 1,
|
|
361
|
+
sourceLineIndex: i,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
return diffLines.map((dl) => {
|
|
365
|
+
if (dl.type === 'removed') {
|
|
366
|
+
const oldIdx = (dl.oldLineNo ?? 1) - 1;
|
|
367
|
+
return {
|
|
368
|
+
type: 'removed' as const,
|
|
369
|
+
html: oldLineHtmls[oldIdx] ?? '',
|
|
370
|
+
lineNo: dl.oldLineNo,
|
|
371
|
+
sourceLineIndex: undefined,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const newIdx = (dl.newLineNo ?? 1) - 1;
|
|
375
|
+
return {
|
|
376
|
+
type: dl.type,
|
|
377
|
+
html: lineHtmls[newIdx] ?? '',
|
|
378
|
+
lineNo: dl.newLineNo,
|
|
379
|
+
sourceLineIndex: newIdx,
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
}, [diffLines, lineHtmls, oldLineHtmls]);
|
|
383
|
+
|
|
384
|
+
// Diff chunks: contiguous groups of changed (added/removed) rows
|
|
385
|
+
const diffChunks = useMemo(() => {
|
|
386
|
+
if (!diffLines) return [];
|
|
387
|
+
const chunks: { startRow: number; endRow: number }[] = [];
|
|
388
|
+
let inChunk = false;
|
|
389
|
+
let start = 0;
|
|
390
|
+
for (let i = 0; i < displayRows.length; i++) {
|
|
391
|
+
const changed = displayRows[i].type !== 'same';
|
|
392
|
+
if (changed && !inChunk) {
|
|
393
|
+
inChunk = true;
|
|
394
|
+
start = i;
|
|
395
|
+
} else if (!changed && inChunk) {
|
|
396
|
+
inChunk = false;
|
|
397
|
+
chunks.push({ startRow: start, endRow: i - 1 });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (inChunk) chunks.push({ startRow: start, endRow: displayRows.length - 1 });
|
|
401
|
+
return chunks;
|
|
402
|
+
}, [diffLines, displayRows]);
|
|
403
|
+
|
|
404
|
+
// Reset active chunk when diff changes
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
setActiveDiffChunk(0);
|
|
407
|
+
}, [diffChunks.length]);
|
|
408
|
+
|
|
409
|
+
const scrollToDiffChunk = useCallback(
|
|
410
|
+
(index: number) => {
|
|
411
|
+
const chunk = diffChunks[index];
|
|
412
|
+
if (!chunk || !tableRef.current) return;
|
|
413
|
+
const scrollParent =
|
|
414
|
+
containerRef.current?.querySelector('.overflow-y-auto') ??
|
|
415
|
+
containerRef.current?.closest('.overflow-y-auto');
|
|
416
|
+
if (!scrollParent) return;
|
|
417
|
+
const rows = tableRef.current.querySelectorAll('.raw-line');
|
|
418
|
+
const targetRow = rows[chunk.startRow];
|
|
419
|
+
if (!targetRow) return;
|
|
420
|
+
const rowRect = targetRow.getBoundingClientRect();
|
|
421
|
+
const parentRect = scrollParent.getBoundingClientRect();
|
|
422
|
+
scrollParent.scrollTo({
|
|
423
|
+
top: scrollParent.scrollTop + rowRect.top - parentRect.top - 40,
|
|
424
|
+
behavior: 'smooth',
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
[diffChunks],
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const handleDiffPrev = useCallback(() => {
|
|
431
|
+
const next = activeDiffChunk > 0 ? activeDiffChunk - 1 : diffChunks.length - 1;
|
|
432
|
+
setActiveDiffChunk(next);
|
|
433
|
+
scrollToDiffChunk(next);
|
|
434
|
+
}, [activeDiffChunk, diffChunks.length, scrollToDiffChunk]);
|
|
435
|
+
|
|
436
|
+
const handleDiffNext = useCallback(() => {
|
|
437
|
+
const next = activeDiffChunk < diffChunks.length - 1 ? activeDiffChunk + 1 : 0;
|
|
438
|
+
setActiveDiffChunk(next);
|
|
439
|
+
scrollToDiffChunk(next);
|
|
440
|
+
}, [activeDiffChunk, diffChunks.length, scrollToDiffChunk]);
|
|
441
|
+
|
|
442
|
+
// Set innerHTML for each line cell and apply search highlights
|
|
443
|
+
useLayoutEffect(() => {
|
|
444
|
+
if (!tableRef.current) return;
|
|
445
|
+
const codeCells = tableRef.current.querySelectorAll<HTMLElement>('.raw-line-content');
|
|
446
|
+
codeCells.forEach((cell, i) => {
|
|
447
|
+
cell.innerHTML = displayRows[i]?.html || '';
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Apply search highlights across all content cells (skip removed lines).
|
|
451
|
+
if (searchQuery) {
|
|
452
|
+
const counts: number[] = [];
|
|
453
|
+
codeCells.forEach((cell, i) => {
|
|
454
|
+
if (displayRows[i]?.type === 'removed') {
|
|
455
|
+
counts.push(0);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const count = highlightSearchMatches(cell, searchQuery, -1);
|
|
459
|
+
counts.push(count);
|
|
460
|
+
});
|
|
461
|
+
const totalCount = counts.reduce((a, b) => a + b, 0);
|
|
462
|
+
|
|
463
|
+
let cumulative = 0;
|
|
464
|
+
codeCells.forEach((cell, i) => {
|
|
465
|
+
if (displayRows[i]?.type === 'removed') return;
|
|
466
|
+
cell.innerHTML = displayRows[i]?.html || '';
|
|
467
|
+
const activeGlobal = searchActiveIndex ?? 0;
|
|
468
|
+
const localActive =
|
|
469
|
+
activeGlobal >= cumulative && activeGlobal < cumulative + counts[i]
|
|
470
|
+
? activeGlobal - cumulative
|
|
471
|
+
: -1;
|
|
472
|
+
highlightSearchMatches(cell, searchQuery, localActive);
|
|
473
|
+
cumulative += counts[i];
|
|
474
|
+
});
|
|
475
|
+
onSearchCount?.(totalCount);
|
|
476
|
+
} else {
|
|
477
|
+
onSearchCount?.(0);
|
|
478
|
+
}
|
|
479
|
+
}, [displayRows, searchQuery, searchActiveIndex, onSearchCount]);
|
|
480
|
+
|
|
481
|
+
// Highlight active comment marker (only in current content, not removed lines)
|
|
482
|
+
useLayoutEffect(() => {
|
|
483
|
+
if (!tableRef.current) return;
|
|
484
|
+
|
|
485
|
+
tableRef.current.querySelectorAll('.raw-comment-marker-active').forEach((el) => {
|
|
486
|
+
el.classList.remove('raw-comment-marker-active');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
if (activeCommentId) {
|
|
490
|
+
const markers = tableRef.current.querySelectorAll(
|
|
491
|
+
`[data-comment-id="${CSS.escape(activeCommentId)}"]`,
|
|
492
|
+
);
|
|
493
|
+
for (const marker of markers) {
|
|
494
|
+
if (!marker.closest('.raw-line-diff-removed')) {
|
|
495
|
+
marker.classList.add('raw-comment-marker-active');
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}, [activeCommentId, displayRows]);
|
|
501
|
+
|
|
502
|
+
/** Find the scrollable container (descendant or ancestor). */
|
|
503
|
+
const getScrollParent = useCallback((): Element | null => {
|
|
504
|
+
if (!containerRef.current) return null;
|
|
505
|
+
// The scroll container is a descendant (flex-1 overflow-y-auto) in pinned toolbar layout
|
|
506
|
+
return (
|
|
507
|
+
containerRef.current.querySelector('.overflow-y-auto') ??
|
|
508
|
+
containerRef.current.closest('.overflow-y-auto')
|
|
509
|
+
);
|
|
510
|
+
}, []);
|
|
511
|
+
|
|
512
|
+
const scrollToComment = useCallback(
|
|
513
|
+
(commentId: string) => {
|
|
514
|
+
if (!tableRef.current || !containerRef.current) return;
|
|
515
|
+
|
|
516
|
+
// Re-enable comment markers if hidden so the marker is visible
|
|
517
|
+
setShowComments(true);
|
|
518
|
+
|
|
519
|
+
// Defer scroll to next frame so display:none is removed first
|
|
520
|
+
requestAnimationFrame(() => {
|
|
521
|
+
if (!tableRef.current) return;
|
|
522
|
+
const marker = tableRef.current.querySelector(
|
|
523
|
+
`[data-comment-id="${CSS.escape(commentId)}"]`,
|
|
524
|
+
);
|
|
525
|
+
if (!marker) return;
|
|
526
|
+
|
|
527
|
+
const scrollParent = getScrollParent();
|
|
528
|
+
if (scrollParent) {
|
|
529
|
+
const markerRect = marker.getBoundingClientRect();
|
|
530
|
+
const parentRect = scrollParent.getBoundingClientRect();
|
|
531
|
+
const offset = markerRect.top - parentRect.top - parentRect.height / 3;
|
|
532
|
+
scrollParent.scrollTop += offset;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Clear previous flash animation
|
|
536
|
+
if (flashTimerRef.current) {
|
|
537
|
+
clearTimeout(flashTimerRef.current);
|
|
538
|
+
tableRef.current?.querySelectorAll('.raw-comment-marker-flash').forEach((el) => {
|
|
539
|
+
el.classList.remove('raw-comment-marker-flash');
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
marker.classList.add('raw-comment-marker-flash');
|
|
543
|
+
flashTimerRef.current = setTimeout(
|
|
544
|
+
() => marker.classList.remove('raw-comment-marker-flash'),
|
|
545
|
+
1500,
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
},
|
|
549
|
+
[getScrollParent],
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const scrollToHeading = useCallback(
|
|
553
|
+
(headingId: string) => {
|
|
554
|
+
if (!tableRef.current) return;
|
|
555
|
+
const headingLine = tableRef.current.querySelector(
|
|
556
|
+
`.raw-line[data-heading-id="${CSS.escape(headingId)}"]`,
|
|
557
|
+
);
|
|
558
|
+
if (!headingLine) return;
|
|
559
|
+
|
|
560
|
+
const scrollParent = getScrollParent();
|
|
561
|
+
if (scrollParent) {
|
|
562
|
+
const lineRect = headingLine.getBoundingClientRect();
|
|
563
|
+
const parentRect = scrollParent.getBoundingClientRect();
|
|
564
|
+
scrollParent.scrollTo({
|
|
565
|
+
top: scrollParent.scrollTop + lineRect.top - parentRect.top,
|
|
566
|
+
behavior: 'smooth',
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
headingLine.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
572
|
+
},
|
|
573
|
+
[getScrollParent],
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
useImperativeHandle(ref, () => ({ scrollToComment, scrollToHeading }), [
|
|
577
|
+
scrollToComment,
|
|
578
|
+
scrollToHeading,
|
|
579
|
+
]);
|
|
580
|
+
|
|
581
|
+
const copyTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
return () => {
|
|
584
|
+
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
|
|
585
|
+
};
|
|
586
|
+
}, []);
|
|
587
|
+
|
|
588
|
+
const showCopyFeedback = useCallback(() => {
|
|
589
|
+
setCopyFeedback(true);
|
|
590
|
+
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
|
|
591
|
+
copyTimerRef.current = setTimeout(() => setCopyFeedback(false), 2000);
|
|
592
|
+
}, []);
|
|
593
|
+
|
|
594
|
+
const handleCopy = useCallback(() => {
|
|
595
|
+
navigator.clipboard.writeText(rawMarkdown).then(showCopyFeedback, () => {});
|
|
596
|
+
}, [rawMarkdown, showCopyFeedback]);
|
|
597
|
+
|
|
598
|
+
const handleCopyWithoutComments = useCallback(() => {
|
|
599
|
+
COMMENT_MARKER_RE.lastIndex = 0;
|
|
600
|
+
const clean = rawMarkdown.replace(COMMENT_MARKER_RE, '');
|
|
601
|
+
navigator.clipboard.writeText(clean).then(showCopyFeedback, () => {});
|
|
602
|
+
}, [rawMarkdown, showCopyFeedback]);
|
|
603
|
+
|
|
604
|
+
const hasChanges = diffLines ? diffLines.some((l) => l.type !== 'same') : true;
|
|
605
|
+
const hasDiffSnapshot = diffSnapshot != null;
|
|
606
|
+
|
|
607
|
+
const toggleClass = (active: boolean) =>
|
|
608
|
+
`p-1 rounded transition-colors ${
|
|
609
|
+
active
|
|
610
|
+
? 'text-primary-text bg-primary-bg'
|
|
611
|
+
: 'text-content-muted hover:text-content-secondary hover:bg-tint'
|
|
612
|
+
}`;
|
|
613
|
+
|
|
614
|
+
const actionClass =
|
|
615
|
+
'text-[11px] rounded px-2 py-0.5 transition-colors text-content-secondary hover:text-content hover:bg-tint';
|
|
616
|
+
|
|
617
|
+
const toolbar = (
|
|
618
|
+
<div className="raw-toolbar">
|
|
619
|
+
<div className="raw-toolbar-left">
|
|
620
|
+
<button
|
|
621
|
+
className={toggleClass(showComments)}
|
|
622
|
+
onClick={() => setShowComments((v) => !v)}
|
|
623
|
+
title={showComments ? 'Hide comment markers' : 'Show comment markers'}
|
|
624
|
+
>
|
|
625
|
+
<svg
|
|
626
|
+
className="w-3.5 h-3.5"
|
|
627
|
+
fill="none"
|
|
628
|
+
viewBox="0 0 24 24"
|
|
629
|
+
stroke="currentColor"
|
|
630
|
+
strokeWidth={2}
|
|
631
|
+
>
|
|
632
|
+
<path
|
|
633
|
+
strokeLinecap="round"
|
|
634
|
+
strokeLinejoin="round"
|
|
635
|
+
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
|
636
|
+
/>
|
|
637
|
+
</svg>
|
|
638
|
+
</button>
|
|
639
|
+
{hasDiffSnapshot && onDiffToggle && (
|
|
640
|
+
<button
|
|
641
|
+
className={`${toggleClass(!!diffEnabled)} flex items-center gap-1`}
|
|
642
|
+
onClick={onDiffToggle}
|
|
643
|
+
title={diffEnabled ? 'Hide diff overlay' : 'Show diff since snapshot'}
|
|
644
|
+
>
|
|
645
|
+
<svg
|
|
646
|
+
className="w-3.5 h-3.5"
|
|
647
|
+
fill="none"
|
|
648
|
+
viewBox="0 0 24 24"
|
|
649
|
+
stroke="currentColor"
|
|
650
|
+
strokeWidth={2}
|
|
651
|
+
>
|
|
652
|
+
<path
|
|
653
|
+
strokeLinecap="round"
|
|
654
|
+
strokeLinejoin="round"
|
|
655
|
+
d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
|
|
656
|
+
/>
|
|
657
|
+
</svg>
|
|
658
|
+
{diffChunks.length > 0 && (
|
|
659
|
+
<span className="text-[10px] tabular-nums">{diffChunks.length}</span>
|
|
660
|
+
)}
|
|
661
|
+
</button>
|
|
662
|
+
)}
|
|
663
|
+
{diffEnabled && diffChunks.length > 0 && (
|
|
664
|
+
<div className="flex items-center gap-0.5 ml-1">
|
|
665
|
+
<button
|
|
666
|
+
className="p-0.5 rounded text-content-muted hover:text-content-secondary hover:bg-tint transition-colors"
|
|
667
|
+
onClick={handleDiffPrev}
|
|
668
|
+
title="Previous change"
|
|
669
|
+
>
|
|
670
|
+
<svg
|
|
671
|
+
className="w-3 h-3"
|
|
672
|
+
fill="none"
|
|
673
|
+
viewBox="0 0 24 24"
|
|
674
|
+
stroke="currentColor"
|
|
675
|
+
strokeWidth={2.5}
|
|
676
|
+
>
|
|
677
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
|
678
|
+
</svg>
|
|
679
|
+
</button>
|
|
680
|
+
<button
|
|
681
|
+
className="p-0.5 rounded text-content-muted hover:text-content-secondary hover:bg-tint transition-colors"
|
|
682
|
+
onClick={handleDiffNext}
|
|
683
|
+
title="Next change"
|
|
684
|
+
>
|
|
685
|
+
<svg
|
|
686
|
+
className="w-3 h-3"
|
|
687
|
+
fill="none"
|
|
688
|
+
viewBox="0 0 24 24"
|
|
689
|
+
stroke="currentColor"
|
|
690
|
+
strokeWidth={2.5}
|
|
691
|
+
>
|
|
692
|
+
<path
|
|
693
|
+
strokeLinecap="round"
|
|
694
|
+
strokeLinejoin="round"
|
|
695
|
+
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
696
|
+
/>
|
|
697
|
+
</svg>
|
|
698
|
+
</button>
|
|
699
|
+
</div>
|
|
700
|
+
)}
|
|
701
|
+
{hasDiffSnapshot && onClearSnapshot && (
|
|
702
|
+
<button className={actionClass} onClick={onClearSnapshot} title="Clear diff snapshot">
|
|
703
|
+
Clear snapshot
|
|
704
|
+
</button>
|
|
705
|
+
)}
|
|
706
|
+
</div>
|
|
707
|
+
<div className="raw-toolbar-right">
|
|
708
|
+
<SplitIconButton
|
|
709
|
+
icon={
|
|
710
|
+
copyFeedback ? (
|
|
711
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
712
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
713
|
+
</svg>
|
|
714
|
+
) : (
|
|
715
|
+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
716
|
+
<path
|
|
717
|
+
strokeLinecap="round"
|
|
718
|
+
strokeLinejoin="round"
|
|
719
|
+
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
|
720
|
+
/>
|
|
721
|
+
</svg>
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
onClick={handleCopy}
|
|
725
|
+
title={copyFeedback ? 'Copied!' : 'Copy document'}
|
|
726
|
+
chevronTitle="Copy options"
|
|
727
|
+
menu={[{ label: 'Copy without comments', onClick: handleCopyWithoutComments }]}
|
|
728
|
+
/>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const containerClass = `raw-view flex flex-col h-full${showComments ? '' : ' raw-view-comments-hidden'}`;
|
|
734
|
+
|
|
735
|
+
if (diffEnabled && diffLines && !hasChanges) {
|
|
736
|
+
return (
|
|
737
|
+
<div ref={containerRef} className={containerClass}>
|
|
738
|
+
{toolbar}
|
|
739
|
+
<div className="flex flex-col items-center justify-center flex-1 text-content-muted px-6">
|
|
740
|
+
<svg
|
|
741
|
+
className="w-12 h-12 mb-3 text-content-faint"
|
|
742
|
+
fill="none"
|
|
743
|
+
viewBox="0 0 24 24"
|
|
744
|
+
stroke="currentColor"
|
|
745
|
+
strokeWidth={1.5}
|
|
746
|
+
>
|
|
747
|
+
<path
|
|
748
|
+
strokeLinecap="round"
|
|
749
|
+
strokeLinejoin="round"
|
|
750
|
+
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
751
|
+
/>
|
|
752
|
+
</svg>
|
|
753
|
+
<p className="text-sm font-medium text-content-secondary mb-1">No changes yet</p>
|
|
754
|
+
<p className="text-xs text-center leading-relaxed max-w-xs">
|
|
755
|
+
This view updates automatically when the file is modified.
|
|
756
|
+
<br />
|
|
757
|
+
Hand off to an agent and changes will appear here.
|
|
758
|
+
</p>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return (
|
|
765
|
+
<div ref={containerRef} className={containerClass}>
|
|
766
|
+
{toolbar}
|
|
767
|
+
|
|
768
|
+
<div className="flex-1 overflow-y-auto px-8 pt-4 pb-[50vh] lg:px-12 xl:px-16">
|
|
769
|
+
<div className="max-w-3xl mx-auto">
|
|
770
|
+
<div ref={tableRef} className="raw-view-table">
|
|
771
|
+
{displayRows.map((row, i) => {
|
|
772
|
+
const diffClass =
|
|
773
|
+
row.type === 'added'
|
|
774
|
+
? 'raw-line-diff-added'
|
|
775
|
+
: row.type === 'removed'
|
|
776
|
+
? 'raw-line-diff-removed'
|
|
777
|
+
: '';
|
|
778
|
+
return (
|
|
779
|
+
<div
|
|
780
|
+
key={i}
|
|
781
|
+
className={`raw-line ${diffClass}`}
|
|
782
|
+
data-heading-id={
|
|
783
|
+
row.sourceLineIndex != null
|
|
784
|
+
? headingIdsByLine.get(row.sourceLineIndex)
|
|
785
|
+
: undefined
|
|
786
|
+
}
|
|
787
|
+
>
|
|
788
|
+
<span className="raw-line-number">{row.lineNo}</span>
|
|
789
|
+
<span className="raw-line-content" />
|
|
790
|
+
</div>
|
|
791
|
+
);
|
|
792
|
+
})}
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
);
|
|
798
|
+
});
|