const { ipcRenderer } = require('electron'); const marked = require('marked'); const DOMPurify = require('dompurify'); const hljs = require('highlight.js'); // Configure marked marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (err) {} } return hljs.highlightAuto(code).value; }, breaks: true, gfm: true }); // Tab Management class TabManager { constructor() { this.tabs = new Map(); this.activeTabId = 1; this.nextTabId = 2; this.isPreviewVisible = true; this.showLineNumbers = false; // Initialize first tab this.tabs.set(1, { id: 1, title: 'Untitled', content: '', filePath: null, isDirty: false, undoStack: [], redoStack: [], findMatches: [], currentMatchIndex: -1 }); this.setupEventListeners(); this.updateUI(); } setupEventListeners() { // Tab bar events document.getElementById('new-tab-btn').addEventListener('click', () => this.createNewTab()); document.getElementById('tab-bar').addEventListener('click', (e) => { if (e.target.classList.contains('tab-close')) { e.stopPropagation(); const tabId = parseInt(e.target.closest('.tab').dataset.tabId); this.closeTab(tabId); } else if (e.target.closest('.tab')) { const tabId = parseInt(e.target.closest('.tab').dataset.tabId); this.switchToTab(tabId); } }); // Editor events for active tab this.setupEditorEvents(); // Toolbar events this.setupToolbarEvents(); // Find dialog events this.setupFindEvents(); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'n': e.preventDefault(); this.createNewTab(); break; case 'w': if (this.tabs.size > 1) { e.preventDefault(); this.closeTab(this.activeTabId); } break; case 't': e.preventDefault(); this.createNewTab(); break; case 'Tab': if (this.tabs.size > 1) { e.preventDefault(); this.switchToNextTab(); } break; } } }); } createNewTab() { const newTabId = this.nextTabId++; const tab = { id: newTabId, title: 'Untitled', content: '', filePath: null, isDirty: false, undoStack: [], redoStack: [], findMatches: [], currentMatchIndex: -1 }; this.tabs.set(newTabId, tab); this.createTabElements(tab); this.switchToTab(newTabId); this.updateTabBar(); } createTabElements(tab) { // Create tab content container const tabContent = document.createElement('div'); tabContent.className = 'tab-content'; tabContent.id = `tab-content-${tab.id}`; tabContent.dataset.tabId = tab.id; tabContent.innerHTML = `
`; document.querySelector('.editor-container').appendChild(tabContent); } switchToTab(tabId) { if (!this.tabs.has(tabId)) return; // Save current tab state before switching if (this.activeTabId && this.tabs.has(this.activeTabId)) { this.saveCurrentTabState(); } this.activeTabId = tabId; this.updateUI(); this.restoreTabState(tabId); this.focusActiveEditor(); // Notify main process about current file for exports const tab = this.tabs.get(tabId); if (tab && tab.filePath) { ipcRenderer.send('set-current-file', tab.filePath); } } switchToNextTab() { const tabIds = Array.from(this.tabs.keys()); const currentIndex = tabIds.indexOf(this.activeTabId); const nextIndex = (currentIndex + 1) % tabIds.length; this.switchToTab(tabIds[nextIndex]); } closeTab(tabId) { if (this.tabs.size === 1) return; // Don't close the last tab const tab = this.tabs.get(tabId); if (tab.isDirty) { // TODO: Show confirmation dialog } // Remove tab elements const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); const tabContent = document.getElementById(`tab-content-${tabId}`); if (tabElement && tabElement.classList.contains('tab')) { tabElement.remove(); } if (tabContent) { tabContent.remove(); } this.tabs.delete(tabId); // Switch to another tab if this was active if (this.activeTabId === tabId) { const remainingTabs = Array.from(this.tabs.keys()); this.switchToTab(remainingTabs[0]); } this.updateTabBar(); } updateTabBar() { const tabBar = document.getElementById('tab-bar'); const existingTabs = tabBar.querySelectorAll('.tab'); // Remove all existing tab elements except the new tab button existingTabs.forEach(tab => tab.remove()); // Add tabs in order const sortedTabs = Array.from(this.tabs.values()).sort((a, b) => a.id - b.id); const newTabBtn = document.getElementById('new-tab-btn'); sortedTabs.forEach(tab => { const tabElement = document.createElement('div'); tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`; tabElement.dataset.tabId = tab.id; const title = tab.filePath ? tab.filePath.split('/').pop() : tab.title; const dirtyIndicator = tab.isDirty ? ' •' : ''; tabElement.innerHTML = ` ${title}${dirtyIndicator} `; tabBar.insertBefore(tabElement, newTabBtn); }); } updateUI() { // Show/hide tab contents document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); const activeContent = document.getElementById(`tab-content-${this.activeTabId}`); if (activeContent) { activeContent.classList.add('active'); } // Update preview visibility this.updatePreviewVisibility(); this.updateLineNumbers(); this.updateTabBar(); } saveCurrentTabState() { const tab = this.tabs.get(this.activeTabId); if (!tab) return; const editor = document.getElementById(`editor-${this.activeTabId}`); if (editor) { tab.content = editor.value; tab.isDirty = tab.content !== (tab.originalContent || ''); } } restoreTabState(tabId) { const tab = this.tabs.get(tabId); if (!tab) return; const editor = document.getElementById(`editor-${tabId}`); const preview = document.getElementById(`preview-${tabId}`); if (editor) { editor.value = tab.content; this.updatePreview(tabId); this.updateWordCount(); } } focusActiveEditor() { const editor = document.getElementById(`editor-${this.activeTabId}`); if (editor) { editor.focus(); } } updatePreview(tabId = this.activeTabId) { const tab = this.tabs.get(tabId); const preview = document.getElementById(`preview-${tabId}`); if (!tab || !preview) return; try { const html = marked.parse(tab.content); const sanitizedHtml = DOMPurify.sanitize(html); preview.innerHTML = sanitizedHtml; } catch (error) { preview.innerHTML = '

Error rendering preview

'; } } updatePreviewVisibility() { document.querySelectorAll('.tab-content').forEach(content => { const previewPane = content.querySelector('.pane:last-child'); const editorPane = content.querySelector('.pane:first-child'); if (this.isPreviewVisible) { previewPane.classList.remove('hidden'); editorPane.classList.remove('full-width'); } else { previewPane.classList.add('hidden'); editorPane.classList.add('full-width'); } }); } updateLineNumbers() { const editor = document.getElementById(`editor-${this.activeTabId}`); const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`); if (!editor || !lineNumbers) return; if (this.showLineNumbers) { const lines = editor.value.split('\\n'); lineNumbers.innerHTML = lines.map((_, i) => `
${i + 1}
` ).join(''); lineNumbers.classList.remove('hidden'); } else { lineNumbers.classList.add('hidden'); } } updateWordCount() { const tab = this.tabs.get(this.activeTabId); if (!tab) return; const words = tab.content.trim().split(/\\s+/).filter(word => word.length > 0).length; const chars = tab.content.length; document.getElementById('word-count').textContent = `Words: ${words} | Characters: ${chars}`; } setupEditorEvents() { // Set up editor events using event delegation document.addEventListener('input', (e) => { if (e.target.classList.contains('editor-textarea')) { const tabId = parseInt(e.target.id.split('-')[1]); if (tabId === this.activeTabId) { this.handleEditorInput(tabId); } } }); document.addEventListener('scroll', (e) => { if (e.target.classList.contains('editor-textarea')) { const tabId = parseInt(e.target.id.split('-')[1]); if (tabId === this.activeTabId) { this.updateLineNumbers(); } } }); } handleEditorInput(tabId) { const tab = this.tabs.get(tabId); if (!tab) return; const editor = document.getElementById(`editor-${tabId}`); tab.content = editor.value; tab.isDirty = true; this.updatePreview(tabId); this.updateWordCount(); this.updateLineNumbers(); this.updateTabBar(); // Add to undo stack this.pushUndoState(tabId); } pushUndoState(tabId) { const tab = this.tabs.get(tabId); if (!tab) return; tab.undoStack.push(tab.content); if (tab.undoStack.length > 50) { tab.undoStack.shift(); } tab.redoStack = []; } undo() { const tab = this.tabs.get(this.activeTabId); if (!tab || tab.undoStack.length === 0) return; tab.redoStack.push(tab.content); tab.content = tab.undoStack.pop(); const editor = document.getElementById(`editor-${this.activeTabId}`); if (editor) { editor.value = tab.content; this.updatePreview(); this.updateWordCount(); } } redo() { const tab = this.tabs.get(this.activeTabId); if (!tab || tab.redoStack.length === 0) return; tab.undoStack.push(tab.content); tab.content = tab.redoStack.pop(); const editor = document.getElementById(`editor-${this.activeTabId}`); if (editor) { editor.value = tab.content; this.updatePreview(); this.updateWordCount(); } } setupToolbarEvents() { // Existing toolbar setup... document.getElementById('btn-preview-toggle').addEventListener('click', () => { this.isPreviewVisible = !this.isPreviewVisible; this.updatePreviewVisibility(); }); document.getElementById('btn-line-numbers').addEventListener('click', () => { this.showLineNumbers = !this.showLineNumbers; this.updateLineNumbers(); }); // Add other toolbar events... } setupFindEvents() { // Find dialog implementation... document.getElementById('btn-find').addEventListener('click', () => { document.getElementById('find-dialog').classList.remove('hidden'); document.getElementById('find-input').focus(); }); document.getElementById('btn-find-close').addEventListener('click', () => { document.getElementById('find-dialog').classList.add('hidden'); }); } // File operations openFile(filePath, content) { let tab = this.tabs.get(this.activeTabId); // If current tab is empty and untitled, reuse it if (!tab.filePath && !tab.isDirty && tab.content === '') { tab.filePath = filePath; tab.title = filePath.split('/').pop(); tab.content = content; tab.originalContent = content; tab.isDirty = false; } else { // Create new tab for the file this.createNewTab(); tab = this.tabs.get(this.activeTabId); tab.filePath = filePath; tab.title = filePath.split('/').pop(); tab.content = content; tab.originalContent = content; tab.isDirty = false; } this.restoreTabState(this.activeTabId); this.updateTabBar(); } getCurrentContent() { const tab = this.tabs.get(this.activeTabId); return tab ? tab.content : ''; } getCurrentFilePath() { const tab = this.tabs.get(this.activeTabId); return tab ? tab.filePath : null; } } // Initialize tab manager let tabManager; document.addEventListener('DOMContentLoaded', () => { tabManager = new TabManager(); // Request current theme ipcRenderer.send('get-theme'); // Set up auto-save interval setInterval(() => { // Auto-save logic for all tabs tabManager.tabs.forEach(tab => { if (tab.isDirty && tab.filePath) { ipcRenderer.send('save-current-file', tab.content); } }); }, 30000); }); // IPC event listeners ipcRenderer.on('file-new', () => { tabManager.createNewTab(); }); ipcRenderer.on('file-opened', (event, data) => { tabManager.openFile(data.path, data.content); }); ipcRenderer.on('file-save', () => { const currentContent = tabManager.getCurrentContent(); const currentFilePath = tabManager.getCurrentFilePath(); if (currentFilePath) { ipcRenderer.send('save-current-file', currentContent); } }); ipcRenderer.on('get-content-for-save', (event, filePath) => { const currentContent = tabManager.getCurrentContent(); ipcRenderer.send('save-file', { path: filePath, content: currentContent }); }); ipcRenderer.on('get-content-for-spreadsheet', (event, format) => { const currentContent = tabManager.getCurrentContent(); ipcRenderer.send('export-spreadsheet', { content: currentContent, format }); }); ipcRenderer.on('toggle-preview', () => { tabManager.isPreviewVisible = !tabManager.isPreviewVisible; tabManager.updatePreviewVisibility(); }); ipcRenderer.on('toggle-find', () => { const findDialog = document.getElementById('find-dialog'); if (findDialog.classList.contains('hidden')) { findDialog.classList.remove('hidden'); document.getElementById('find-input').focus(); } else { findDialog.classList.add('hidden'); } }); ipcRenderer.on('theme-changed', (event, theme) => { document.body.className = `theme-${theme}`; });