mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-08 23:47:37 +00:00
refator(code-editor): make CodeEditor feature based component
- replaced interfaces with types from main-content types
This commit is contained in:
141
src/components/code-editor/utils/editorExtensions.ts
Normal file
141
src/components/code-editor/utils/editorExtensions.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { getChunks } from '@codemirror/merge';
|
||||
import { EditorView, ViewPlugin } from '@codemirror/view';
|
||||
import { showMinimap } from '@replit/codemirror-minimap';
|
||||
import type { CodeEditorFile } from '../types/types';
|
||||
|
||||
// Lightweight lexer for `.env` files (including `.env.*` variants).
|
||||
const envLanguage = StreamLanguage.define({
|
||||
token(stream) {
|
||||
if (stream.match(/^#.*/)) return 'comment';
|
||||
if (stream.sol() && stream.match(/^[A-Za-z_][A-Za-z0-9_.]*(?==)/)) return 'variableName.definition';
|
||||
if (stream.match(/^=/)) return 'operator';
|
||||
if (stream.match(/^"(?:[^"\\]|\\.)*"?/)) return 'string';
|
||||
if (stream.match(/^'(?:[^'\\]|\\.)*'?/)) return 'string';
|
||||
if (stream.match(/^\$\{[^}]*\}?/)) return 'variableName.special';
|
||||
if (stream.match(/^\$[A-Za-z_][A-Za-z0-9_]*/)) return 'variableName.special';
|
||||
if (stream.match(/^\d+/)) return 'number';
|
||||
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
export const getLanguageExtensions = (filename: string) => {
|
||||
const lowerName = filename.toLowerCase();
|
||||
if (lowerName === '.env' || lowerName.startsWith('.env.')) {
|
||||
return [envLanguage];
|
||||
}
|
||||
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'js':
|
||||
case 'jsx':
|
||||
case 'ts':
|
||||
case 'tsx':
|
||||
return [javascript({ jsx: true, typescript: ext.includes('ts') })];
|
||||
case 'py':
|
||||
return [python()];
|
||||
case 'html':
|
||||
case 'htm':
|
||||
return [html()];
|
||||
case 'css':
|
||||
case 'scss':
|
||||
case 'less':
|
||||
return [css()];
|
||||
case 'json':
|
||||
return [json()];
|
||||
case 'md':
|
||||
case 'markdown':
|
||||
return [markdown()];
|
||||
case 'env':
|
||||
return [envLanguage];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const createMinimapExtension = ({
|
||||
file,
|
||||
showDiff,
|
||||
minimapEnabled,
|
||||
isDarkMode,
|
||||
}: {
|
||||
file: CodeEditorFile;
|
||||
showDiff: boolean;
|
||||
minimapEnabled: boolean;
|
||||
isDarkMode: boolean;
|
||||
}) => {
|
||||
if (!file.diffInfo || !showDiff || !minimapEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const gutters: Record<number, string> = {};
|
||||
|
||||
return [
|
||||
showMinimap.compute(['doc'], (state) => {
|
||||
const chunksData = getChunks(state);
|
||||
const chunks = chunksData?.chunks || [];
|
||||
|
||||
Object.keys(gutters).forEach((key) => {
|
||||
delete gutters[Number(key)];
|
||||
});
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
const fromLine = state.doc.lineAt(chunk.fromB).number;
|
||||
const toLine = state.doc.lineAt(Math.min(chunk.toB, state.doc.length)).number;
|
||||
|
||||
for (let lineNumber = fromLine; lineNumber <= toLine; lineNumber += 1) {
|
||||
gutters[lineNumber] = isDarkMode ? 'rgba(34, 197, 94, 0.8)' : 'rgba(34, 197, 94, 1)';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
create: () => ({ dom: document.createElement('div') }),
|
||||
displayText: 'blocks',
|
||||
showOverlay: 'always',
|
||||
gutters: [gutters],
|
||||
};
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export const createScrollToFirstChunkExtension = ({
|
||||
file,
|
||||
showDiff,
|
||||
}: {
|
||||
file: CodeEditorFile;
|
||||
showDiff: boolean;
|
||||
}) => {
|
||||
if (!file.diffInfo || !showDiff) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
ViewPlugin.fromClass(class {
|
||||
constructor(view: EditorView) {
|
||||
// Wait for merge decorations so the first chunk location is stable.
|
||||
setTimeout(() => {
|
||||
const chunksData = getChunks(view.state);
|
||||
const firstChunk = chunksData?.chunks?.[0];
|
||||
|
||||
if (firstChunk) {
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(firstChunk.fromB, { y: 'center' }),
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
update() {}
|
||||
|
||||
destroy() {}
|
||||
}),
|
||||
];
|
||||
};
|
||||
79
src/components/code-editor/utils/editorStyles.ts
Normal file
79
src/components/code-editor/utils/editorStyles.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export const getEditorLoadingStyles = (isDarkMode: boolean) => {
|
||||
return `
|
||||
.code-editor-loading {
|
||||
background-color: ${isDarkMode ? '#111827' : '#ffffff'} !important;
|
||||
}
|
||||
|
||||
.code-editor-loading:hover {
|
||||
background-color: ${isDarkMode ? '#111827' : '#ffffff'} !important;
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export const getEditorStyles = (isDarkMode: boolean) => {
|
||||
return `
|
||||
.cm-deletedChunk {
|
||||
background-color: ${isDarkMode ? 'rgba(239, 68, 68, 0.15)' : 'rgba(255, 235, 235, 1)'} !important;
|
||||
border-left: 3px solid ${isDarkMode ? 'rgba(239, 68, 68, 0.6)' : 'rgb(239, 68, 68)'} !important;
|
||||
padding-left: 4px !important;
|
||||
}
|
||||
|
||||
.cm-insertedChunk {
|
||||
background-color: ${isDarkMode ? 'rgba(34, 197, 94, 0.15)' : 'rgba(230, 255, 237, 1)'} !important;
|
||||
border-left: 3px solid ${isDarkMode ? 'rgba(34, 197, 94, 0.6)' : 'rgb(34, 197, 94)'} !important;
|
||||
padding-left: 4px !important;
|
||||
}
|
||||
|
||||
.cm-editor.cm-merge-b .cm-changedText {
|
||||
background: ${isDarkMode ? 'rgba(34, 197, 94, 0.4)' : 'rgba(34, 197, 94, 0.3)'} !important;
|
||||
padding-top: 2px !important;
|
||||
padding-bottom: 2px !important;
|
||||
margin-top: -2px !important;
|
||||
margin-bottom: -2px !important;
|
||||
}
|
||||
|
||||
.cm-editor .cm-deletedChunk .cm-changedText {
|
||||
background: ${isDarkMode ? 'rgba(239, 68, 68, 0.4)' : 'rgba(239, 68, 68, 0.3)'} !important;
|
||||
padding-top: 2px !important;
|
||||
padding-bottom: 2px !important;
|
||||
margin-top: -2px !important;
|
||||
margin-bottom: -2px !important;
|
||||
}
|
||||
|
||||
.cm-gutter.cm-gutter-minimap {
|
||||
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
|
||||
}
|
||||
|
||||
.cm-editor-toolbar-panel {
|
||||
padding: 4px 10px;
|
||||
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
|
||||
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
|
||||
color: ${isDarkMode ? '#d1d5db' : '#374151'};
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cm-diff-nav-btn,
|
||||
.cm-toolbar-btn {
|
||||
padding: 3px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cm-diff-nav-btn:hover,
|
||||
.cm-toolbar-btn:hover {
|
||||
background-color: ${isDarkMode ? '#374151' : '#f3f4f6'};
|
||||
}
|
||||
|
||||
.cm-diff-nav-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
};
|
||||
189
src/components/code-editor/utils/editorToolbarPanel.ts
Normal file
189
src/components/code-editor/utils/editorToolbarPanel.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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 '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />';
|
||||
}
|
||||
|
||||
return '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />';
|
||||
};
|
||||
|
||||
const getExpandIcon = (isExpanded: boolean) => {
|
||||
if (isExpanded) {
|
||||
return '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />';
|
||||
}
|
||||
|
||||
return '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />';
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
let toolbarHtml = '<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">';
|
||||
toolbarHtml += '<div style="display: flex; align-items: center; gap: 8px;">';
|
||||
|
||||
if (hasDiff) {
|
||||
toolbarHtml += `
|
||||
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} ${labels.changes}</span>
|
||||
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="${labels.previousChange}" ${chunkCount === 0 ? 'disabled' : ''}>
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="cm-diff-nav-btn cm-diff-nav-next" title="${labels.nextChange}" ${chunkCount === 0 ? 'disabled' : ''}>
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
toolbarHtml += '</div>';
|
||||
toolbarHtml += '<div style="display: flex; align-items: center; gap: 4px;">';
|
||||
|
||||
if (file.diffInfo) {
|
||||
toolbarHtml += `
|
||||
<button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? labels.hideDiff : labels.showDiff}">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
${getDiffVisibilityIcon(showDiff)}
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (isSidebar && onPopOut) {
|
||||
toolbarHtml += `
|
||||
<button class="cm-toolbar-btn cm-popout-btn" title="Open in modal">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" />
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
if (isSidebar && onToggleExpand) {
|
||||
toolbarHtml += `
|
||||
<button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? labels.collapse : labels.expand}">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
${getExpandIcon(isExpanded)}
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
toolbarHtml += '</div>';
|
||||
toolbarHtml += '</div>';
|
||||
|
||||
dom.innerHTML = toolbarHtml;
|
||||
|
||||
if (hasDiff) {
|
||||
const previousButton = dom.querySelector<HTMLButtonElement>('.cm-diff-nav-prev');
|
||||
const nextButton = dom.querySelector<HTMLButtonElement>('.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<HTMLButtonElement>('.cm-toggle-diff-btn');
|
||||
toggleDiffButton?.addEventListener('click', onToggleDiff);
|
||||
|
||||
const popOutButton = dom.querySelector<HTMLButtonElement>('.cm-popout-btn');
|
||||
popOutButton?.addEventListener('click', () => {
|
||||
onPopOut?.();
|
||||
});
|
||||
|
||||
const expandButton = dom.querySelector<HTMLButtonElement>('.cm-expand-btn');
|
||||
expandButton?.addEventListener('click', () => {
|
||||
onToggleExpand?.();
|
||||
});
|
||||
};
|
||||
|
||||
updatePanel();
|
||||
|
||||
return {
|
||||
top: true,
|
||||
dom,
|
||||
update: updatePanel,
|
||||
};
|
||||
};
|
||||
|
||||
return [showPanel.of(createPanel)];
|
||||
};
|
||||
Reference in New Issue
Block a user