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}`;
});