import { getChunks } from '@codemirror/merge'; import { EditorView, showPanel } from '@codemirror/view'; import type { CodeEditorFile } from '../types/types'; type EditorToolbarLabels = { changes: string; previousChange: string; nextChange: string; hideDiff: string; showDiff: string; collapse: string; expand: string; }; type CreateEditorToolbarPanelParams = { file: CodeEditorFile; showDiff: boolean; isSidebar: boolean; isExpanded: boolean; onToggleDiff: () => void; onPopOut: (() => void) | null; onToggleExpand: (() => void) | null; labels: EditorToolbarLabels; }; const getDiffVisibilityIcon = (showDiff: boolean) => { if (showDiff) { return ''; } return ''; }; const getExpandIcon = (isExpanded: boolean) => { if (isExpanded) { return ''; } return ''; }; const escapeHtml = (value: string): string => ( value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') ); export const createEditorToolbarPanelExtension = ({ file, showDiff, isSidebar, isExpanded, onToggleDiff, onPopOut, onToggleExpand, labels, }: CreateEditorToolbarPanelParams) => { const hasToolbarButtons = Boolean(file.diffInfo || (isSidebar && onPopOut) || (isSidebar && onToggleExpand)); if (!hasToolbarButtons) { return []; } const createPanel = (view: EditorView) => { const dom = document.createElement('div'); dom.className = 'cm-editor-toolbar-panel'; let currentIndex = 0; const updatePanel = () => { const hasDiff = Boolean(file.diffInfo && showDiff); const chunksData = hasDiff ? getChunks(view.state) : null; const chunks = chunksData?.chunks || []; const chunkCount = chunks.length; const maxChunkIndex = Math.max(0, chunkCount - 1); currentIndex = Math.max(0, Math.min(currentIndex, maxChunkIndex)); const escapedLabels = { changes: escapeHtml(labels.changes), previousChange: escapeHtml(labels.previousChange), nextChange: escapeHtml(labels.nextChange), hideDiff: escapeHtml(labels.hideDiff), showDiff: escapeHtml(labels.showDiff), collapse: escapeHtml(labels.collapse), expand: escapeHtml(labels.expand), }; // Icons are static SVG path fragments controlled by this module. const diffVisibilityIcon = getDiffVisibilityIcon(showDiff); const expandIcon = getExpandIcon(isExpanded); let toolbarHtml = '
'; toolbarHtml += '
'; if (hasDiff) { toolbarHtml += ` ${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} ${escapedLabels.changes} `; } toolbarHtml += '
'; toolbarHtml += '
'; if (file.diffInfo) { toolbarHtml += ` `; } if (isSidebar && onPopOut) { toolbarHtml += ` `; } if (isSidebar && onToggleExpand) { toolbarHtml += ` `; } toolbarHtml += '
'; toolbarHtml += '
'; dom.innerHTML = toolbarHtml; if (hasDiff) { const previousButton = dom.querySelector('.cm-diff-nav-prev'); const nextButton = dom.querySelector('.cm-diff-nav-next'); previousButton?.addEventListener('click', () => { if (chunks.length === 0) { return; } currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1; const chunk = chunks[currentIndex]; if (chunk) { view.dispatch({ effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }), }); } updatePanel(); }); nextButton?.addEventListener('click', () => { if (chunks.length === 0) { return; } currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; const chunk = chunks[currentIndex]; if (chunk) { view.dispatch({ effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }), }); } updatePanel(); }); } const toggleDiffButton = dom.querySelector('.cm-toggle-diff-btn'); toggleDiffButton?.addEventListener('click', onToggleDiff); const popOutButton = dom.querySelector('.cm-popout-btn'); popOutButton?.addEventListener('click', () => { onPopOut?.(); }); const expandButton = dom.querySelector('.cm-expand-btn'); expandButton?.addEventListener('click', () => { onToggleExpand?.(); }); }; updatePanel(); return { top: true, dom, update: updatePanel, }; }; return [showPanel.of(createPanel)]; };