From fefcc0f338b22efdd62d77935a7c0ab1c1427da2 Mon Sep 17 00:00:00 2001 From: simos Date: Fri, 31 Oct 2025 12:11:47 +0000 Subject: [PATCH] feat(editor): Move code editor preferences to settings and add option to expand editor Add global settings integration and persistent user preferences for the code editor. Settings are now stored in localStorage and persist across sessions. Changes: - Add theme, word wrap, minimap, line numbers, and font size settings - Load editor preferences from localStorage on initialization - Expose global openSettings function for cross-component access - Add settingsInitialTab state to control which settings tab opens - Pass initialTab prop to Settings component for navigation This improves UX by remembering user preferences and allows other components to open settings to specific tabs programmatically. --- server/routes/git.js | 18 +- src/App.jsx | 8 + src/components/CodeEditor.jsx | 283 ++++++++++++++++++-------- src/components/MainContent.jsx | 36 ++-- src/components/QuickSettingsPanel.jsx | 10 +- src/components/Settings.jsx | 209 ++++++++++++++++++- 6 files changed, 446 insertions(+), 118 deletions(-) diff --git a/server/routes/git.js b/server/routes/git.js index da917d0..0f4f10d 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -502,16 +502,16 @@ router.post('/generate-commit-message', async (req, res) => { */ async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) { // Create the prompt - const prompt = `You are a git commit message generator. Based on the following file changes and diffs, generate a commit message in conventional commit format. + const prompt = `Generate a conventional commit message for these changes. REQUIREMENTS: -- Use conventional commit format: type(scope): subject -- Include a body that explains what changed and why -- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore -- Keep subject line under 50 characters -- Wrap body at 72 characters -- Be specific and descriptive -- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks +- Format: type(scope): subject +- Include body explaining what changed and why +- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore +- Subject under 50 chars, body wrapped at 72 chars +- Focus on user-facing changes, not implementation details +- Consider what's being added AND removed +- Return ONLY the commit message (no markdown, explanations, or code blocks) FILES CHANGED: ${files.map(f => `- ${f}`).join('\n')} @@ -519,7 +519,7 @@ ${files.map(f => `- ${f}`).join('\n')} DIFFS: ${diffContext.substring(0, 4000)} -Generate the commit message now:`; +Generate the commit message:`; try { // Create a simple writer that collects the response diff --git a/src/App.jsx b/src/App.jsx index 3a84f0f..4d10c9c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -55,6 +55,7 @@ function AppContent() { const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [isInputFocused, setIsInputFocused] = useState(false); const [showSettings, setShowSettings] = useState(false); + const [settingsInitialTab, setSettingsInitialTab] = useState('tools'); const [showQuickSettings, setShowQuickSettings] = useState(false); const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false); const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false); @@ -308,6 +309,12 @@ function AppContent() { // Expose fetchProjects globally for component access window.refreshProjects = fetchProjects; + // Expose openSettings function globally for component access + window.openSettings = useCallback((tab = 'tools') => { + setSettingsInitialTab(tab); + setShowSettings(true); + }, []); + // Handle URL-based session loading useEffect(() => { if (sessionId && projects.length > 0) { @@ -927,6 +934,7 @@ function AppContent() { isOpen={showSettings} onClose={() => setShowSettings(false)} projects={projects} + initialTab={settingsInitialTab} /> {/* Version Upgrade Modal */} diff --git a/src/components/CodeEditor.jsx b/src/components/CodeEditor.jsx index 770a1c8..172242d 100644 --- a/src/components/CodeEditor.jsx +++ b/src/components/CodeEditor.jsx @@ -10,23 +10,37 @@ import { oneDark } from '@codemirror/theme-one-dark'; import { EditorView, showPanel, ViewPlugin } from '@codemirror/view'; import { unifiedMergeView, getChunks } from '@codemirror/merge'; import { showMinimap } from '@replit/codemirror-minimap'; -import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react'; +import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react'; import { api } from '../utils/api'; -function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { +function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) { const [content, setContent] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); - const [isDarkMode, setIsDarkMode] = useState(true); + const [isDarkMode, setIsDarkMode] = useState(() => { + const savedTheme = localStorage.getItem('codeEditorTheme'); + return savedTheme ? savedTheme === 'dark' : true; + }); const [saveSuccess, setSaveSuccess] = useState(false); const [showDiff, setShowDiff] = useState(!!file.diffInfo); - const [wordWrap, setWordWrap] = useState(false); + const [wordWrap, setWordWrap] = useState(() => { + return localStorage.getItem('codeEditorWordWrap') === 'true'; + }); + const [minimapEnabled, setMinimapEnabled] = useState(() => { + return localStorage.getItem('codeEditorShowMinimap') !== 'false'; + }); + const [showLineNumbers, setShowLineNumbers] = useState(() => { + return localStorage.getItem('codeEditorLineNumbers') !== 'false'; + }); + const [fontSize, setFontSize] = useState(() => { + return localStorage.getItem('codeEditorFontSize') || '14'; + }); const editorRef = useRef(null); // Create minimap extension with chunk-based gutters const minimapExtension = useMemo(() => { - if (!file.diffInfo || !showDiff) return []; + if (!file.diffInfo || !showDiff || !minimapEnabled) return []; const gutters = {}; @@ -58,7 +72,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { }; }) ]; - }, [file.diffInfo, showDiff, isDarkMode]); + }, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]); // Create extension to scroll to first chunk on mount const scrollToFirstChunkExtension = useMemo(() => { @@ -89,24 +103,28 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { ]; }, [file.diffInfo, showDiff]); - // Create diff navigation panel extension - const diffNavigationPanel = useMemo(() => { - if (!file.diffInfo || !showDiff) return []; - + // Create editor toolbar panel - always visible + const editorToolbarPanel = useMemo(() => { const createPanel = (view) => { const dom = document.createElement('div'); - dom.className = 'cm-diff-navigation-panel'; + dom.className = 'cm-editor-toolbar-panel'; let currentIndex = 0; const updatePanel = () => { - // Use getChunks API to get ALL chunks regardless of viewport - const chunksData = getChunks(view.state); + // Check if we have diff info and it's enabled + const hasDiff = file.diffInfo && showDiff; + const chunksData = hasDiff ? getChunks(view.state) : null; const chunks = chunksData?.chunks || []; const chunkCount = chunks.length; - dom.innerHTML = ` -
+ // Build the toolbar HTML + let toolbarHTML = '
'; + + // Left side - diff navigation (if applicable) + toolbarHTML += '
'; + if (hasDiff) { + toolbarHTML += ` ${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes -
+ `; + } + toolbarHTML += '
'; + + // Right side - action buttons + toolbarHTML += '
'; + + // Show/hide diff button (only if there's diff info) + if (file.diffInfo) { + toolbarHTML += ` + + `; + } + + // Settings button + toolbarHTML += ` + `; - const prevBtn = dom.querySelector('.cm-diff-nav-prev'); - const nextBtn = dom.querySelector('.cm-diff-nav-next'); + // Expand button (only in sidebar mode) + if (isSidebar && onToggleExpand) { + toolbarHTML += ` + + `; + } - prevBtn?.addEventListener('click', () => { - if (chunks.length === 0) return; - currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1; + toolbarHTML += '
'; + toolbarHTML += '
'; - // Navigate to the chunk - use fromB which is the position in the current document - const chunk = chunks[currentIndex]; - if (chunk) { - // Scroll to the start of the chunk in the B side (current document) - view.dispatch({ - effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) - }); + dom.innerHTML = toolbarHTML; + + // Attach event listeners for diff navigation + if (hasDiff) { + const prevBtn = dom.querySelector('.cm-diff-nav-prev'); + const nextBtn = dom.querySelector('.cm-diff-nav-next'); + + prevBtn?.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(); + }); + + nextBtn?.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(); + }); + } + + // Attach event listener for toggle diff button + if (file.diffInfo) { + const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn'); + toggleDiffBtn?.addEventListener('click', () => { + setShowDiff(!showDiff); + }); + } + + // Attach event listener for settings button + const settingsBtn = dom.querySelector('.cm-settings-btn'); + settingsBtn?.addEventListener('click', () => { + if (window.openSettings) { + window.openSettings('appearance'); } - updatePanel(); }); - nextBtn?.addEventListener('click', () => { - if (chunks.length === 0) return; - currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; - - // Navigate to the chunk - use fromB which is the position in the current document - const chunk = chunks[currentIndex]; - if (chunk) { - // Scroll to the start of the chunk in the B side (current document) - view.dispatch({ - effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) - }); - } - updatePanel(); - }); + // Attach event listener for expand button + if (isSidebar && onToggleExpand) { + const expandBtn = dom.querySelector('.cm-expand-btn'); + expandBtn?.addEventListener('click', () => { + onToggleExpand(); + }); + } }; updatePanel(); @@ -165,7 +252,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { }; return [showPanel.of(createPanel)]; - }, [file.diffInfo, showDiff]); + }, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]); // Get language extension based on file extension const getLanguageExtension = (filename) => { @@ -290,6 +377,57 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { setIsFullscreen(!isFullscreen); }; + // Save theme preference to localStorage + useEffect(() => { + localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light'); + }, [isDarkMode]); + + // Save word wrap preference to localStorage + useEffect(() => { + localStorage.setItem('codeEditorWordWrap', wordWrap.toString()); + }, [wordWrap]); + + // Listen for settings changes from the Settings modal + useEffect(() => { + const handleStorageChange = () => { + const newTheme = localStorage.getItem('codeEditorTheme'); + if (newTheme) { + setIsDarkMode(newTheme === 'dark'); + } + + const newWordWrap = localStorage.getItem('codeEditorWordWrap'); + if (newWordWrap !== null) { + setWordWrap(newWordWrap === 'true'); + } + + const newShowMinimap = localStorage.getItem('codeEditorShowMinimap'); + if (newShowMinimap !== null) { + setMinimapEnabled(newShowMinimap !== 'false'); + } + + const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers'); + if (newShowLineNumbers !== null) { + setShowLineNumbers(newShowLineNumbers !== 'false'); + } + + const newFontSize = localStorage.getItem('codeEditorFontSize'); + if (newFontSize) { + setFontSize(newFontSize); + } + }; + + // Listen for storage events (changes from other tabs/windows) + window.addEventListener('storage', handleStorageChange); + + // Custom event for same-window updates + window.addEventListener('codeEditorSettingsChanged', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('codeEditorSettingsChanged', handleStorageChange); + }; + }, []); + // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (e) => { @@ -329,7 +467,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { ) : ( -
+
@@ -381,8 +519,8 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'}; } - /* Diff navigation panel styling */ - .cm-diff-navigation-panel { + /* Editor toolbar panel styling */ + .cm-editor-toolbar-panel { padding: 8px 12px; background-color: ${isDarkMode ? '#1f2937' : '#ffffff'}; border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'}; @@ -390,7 +528,8 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { font-size: 14px; } - .cm-diff-nav-btn { + .cm-diff-nav-btn, + .cm-toolbar-btn { padding: 4px; background: transparent; border: none; @@ -400,9 +539,11 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { align-items: center; justify-content: center; color: inherit; + transition: background-color 0.2s; } - .cm-diff-nav-btn:hover { + .cm-diff-nav-btn:hover, + .cm-toolbar-btn:hover { background-color: ${isDarkMode ? '#374151' : '#f3f4f6'}; } @@ -414,7 +555,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
@@ -433,7 +574,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {

{file.name}

{file.diffInfo && ( - 📝 Has changes + Showing changes )}
@@ -442,36 +583,6 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
- {file.diffInfo && ( - - )} - - - - - +
+
+ + {/* Word Wrap */} +
+
+
+
+ Word Wrap +
+
+ Enable word wrapping by default in the editor +
+
+ +
+
+ + {/* Show Minimap */} +
+
+
+
+ Show Minimap +
+
+ Display a minimap for easier navigation in diff view +
+
+ +
+
+ + {/* Show Line Numbers */} +
+
+
+
+ Show Line Numbers +
+
+ Display line numbers in the editor +
+
+ +
+
+ + {/* Font Size */} +
+
+
+
+ Font Size +
+
+ Editor font size in pixels +
+
+ +
+
+
)} @@ -818,7 +1015,7 @@ function Settings({ isOpen, onClose, projects = [] }) { type="checkbox" checked={skipPermissions} onChange={(e) => setSkipPermissions(e.target.checked)} - className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" + className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600" />
@@ -1578,7 +1775,7 @@ function Settings({ isOpen, onClose, projects = [] }) { type="checkbox" checked={cursorSkipPermissions} onChange={(e) => setCursorSkipPermissions(e.target.checked)} - className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" + className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600" />