mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
Merge master with comprehensive v1.2.0 editor enhancements
This commit is contained in:
@@ -6,15 +6,27 @@ A cross-platform Markdown editor and converter powered by Pandoc.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 📝 **Rich Markdown Editor** - Full-featured editor with syntax highlighting and toolbar
|
### ✨ Advanced Markdown Editor
|
||||||
- 👁️ **Live Preview** - See your markdown rendered in real-time
|
- 📝 **Rich Text Editor** - Full-featured editor with syntax highlighting and comprehensive toolbar
|
||||||
|
- 🔍 **Find & Replace** - Powerful search and replace with match highlighting and navigation
|
||||||
|
- 🔢 **Line Numbers** - Toggle line numbers for easier code editing and navigation
|
||||||
|
- ↩️ **Smart Auto-Indentation** - Automatic list continuation and intelligent indentation
|
||||||
|
- ⏪ **Undo/Redo** - Full undo/redo support with keyboard shortcuts
|
||||||
|
- ⌨️ **Advanced Shortcuts** - Tab indentation, line selection, and smart text formatting
|
||||||
|
|
||||||
|
### 🎨 Themes & Interface
|
||||||
|
- 👁️ **Live Preview** - See your markdown rendered in real-time with synchronized scrolling
|
||||||
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes
|
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes
|
||||||
- 📤 **Document Export** - Convert to HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP)
|
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
|
||||||
|
|
||||||
|
### 📤 Export & Conversion
|
||||||
|
- 📄 **Document Export** - Convert to HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP)
|
||||||
- 📊 **Spreadsheet Export** - Export markdown tables to Excel (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats
|
- 📊 **Spreadsheet Export** - Export markdown tables to Excel (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats
|
||||||
- 📥 **Document Import & Conversion** - Import various document formats and convert between formats via Pandoc
|
- 📥 **Document Import & Conversion** - Import various document formats and convert between formats via Pandoc
|
||||||
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation
|
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation
|
||||||
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
|
|
||||||
- 🖥️ **Cross-Platform** - Works on Windows, macOS, and Linux
|
### 🖥️ Platform Support
|
||||||
|
- **Cross-Platform** - Works seamlessly on Windows, macOS, and Linux
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -65,14 +77,25 @@ npm start
|
|||||||
|
|
||||||
## Keyboard Shortcuts
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
### File Operations
|
||||||
- `Ctrl/Cmd + N` - New file
|
- `Ctrl/Cmd + N` - New file
|
||||||
- `Ctrl/Cmd + O` - Open file
|
- `Ctrl/Cmd + O` - Open file
|
||||||
- `Ctrl/Cmd + S` - Save file
|
- `Ctrl/Cmd + S` - Save file
|
||||||
- `Ctrl/Cmd + Shift + S` - Save as
|
- `Ctrl/Cmd + Shift + S` - Save as
|
||||||
- `Ctrl/Cmd + I` - Import document
|
- `Ctrl/Cmd + I` - Import document
|
||||||
|
|
||||||
|
### Editor Features
|
||||||
|
- `Ctrl/Cmd + F` - Find & Replace
|
||||||
|
- `Ctrl/Cmd + Z` - Undo
|
||||||
|
- `Ctrl/Cmd + Shift + Z` - Redo
|
||||||
|
- `Tab` - Indent lines or insert 4 spaces
|
||||||
|
- `Shift + Tab` - Outdent selected lines
|
||||||
|
- `Enter` - Auto-continue lists with proper indentation
|
||||||
|
|
||||||
|
### View & Navigation
|
||||||
- `Ctrl/Cmd + P` - Toggle preview
|
- `Ctrl/Cmd + P` - Toggle preview
|
||||||
- `Ctrl/Cmd + Enter` - Toggle preview (alternative)
|
- `Ctrl/Cmd + Enter` - Toggle preview (alternative)
|
||||||
- `Tab` - Insert 4 spaces (in editor)
|
- `Escape` - Close find dialog
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -97,7 +120,7 @@ npm run dist:all
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
- **v1.2.0** - Added PowerPoint export, document conversion menu, table creation helper, spreadsheet export
|
- **v1.2.0** - Major editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
|
||||||
- **v1.1.0** - Added Excel/ODS spreadsheet export, updated author information, renamed to PanConverter
|
- **v1.1.0** - Added Excel/ODS spreadsheet export, updated author information, renamed to PanConverter
|
||||||
- **v1.0.0** - Initial release with basic markdown editing, themes, and Pandoc export
|
- **v1.0.0** - Initial release with basic markdown editing, themes, and Pandoc export
|
||||||
|
|
||||||
|
|||||||
+34
-1
@@ -69,6 +69,20 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="toolbar-separator"></div>
|
<div class="toolbar-separator"></div>
|
||||||
|
<button id="btn-find" title="Find & Replace (Ctrl+F)">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.35-4.35"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="btn-line-numbers" title="Toggle Line Numbers">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M3 6h18"></path>
|
||||||
|
<path d="M3 12h18"></path>
|
||||||
|
<path d="M3 18h18"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="toolbar-separator"></div>
|
||||||
<button id="btn-preview-toggle" title="Toggle Preview">
|
<button id="btn-preview-toggle" title="Toggle Preview">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||||
@@ -77,9 +91,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Find & Replace Dialog -->
|
||||||
|
<div id="find-dialog" class="find-dialog hidden">
|
||||||
|
<div class="find-controls">
|
||||||
|
<input type="text" id="find-input" placeholder="Find...">
|
||||||
|
<input type="text" id="replace-input" placeholder="Replace...">
|
||||||
|
<button id="btn-find-prev" title="Previous">↑</button>
|
||||||
|
<button id="btn-find-next" title="Next">↓</button>
|
||||||
|
<button id="btn-replace" title="Replace">Replace</button>
|
||||||
|
<button id="btn-replace-all" title="Replace All">All</button>
|
||||||
|
<button id="btn-find-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="find-info">
|
||||||
|
<span id="find-count">0 matches</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<div id="editor-pane" class="pane">
|
<div id="editor-pane" class="pane">
|
||||||
<textarea id="editor"></textarea>
|
<div class="editor-wrapper">
|
||||||
|
<div id="line-numbers" class="line-numbers hidden"></div>
|
||||||
|
<textarea id="editor"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="preview-pane" class="pane">
|
<div id="preview-pane" class="pane">
|
||||||
<div id="preview"></div>
|
<div id="preview"></div>
|
||||||
|
|||||||
+9
-3
@@ -107,12 +107,18 @@ function createMenu() {
|
|||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
|
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
|
||||||
{ label: 'Redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
|
{ label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', role: 'redo' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
|
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
|
||||||
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
|
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
|
||||||
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
|
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
|
||||||
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }
|
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Find & Replace',
|
||||||
|
accelerator: 'CmdOrCtrl+F',
|
||||||
|
click: () => mainWindow.webContents.send('toggle-find')
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -178,7 +184,7 @@ function createMenu() {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'About PanConverter',
|
title: 'About PanConverter',
|
||||||
message: 'PanConverter',
|
message: 'PanConverter',
|
||||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.2.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Markdown editing with live preview\n• Export to multiple formats via Pandoc\n• Export tables to Excel/ODS spreadsheets\n• PowerPoint & presentation export\n• Document import & conversion\n• Table creation helper\n• Multiple themes support',
|
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.2.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Advanced markdown editing with live preview\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality',
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+324
-3
@@ -24,11 +24,22 @@ const previewPane = document.getElementById('preview-pane');
|
|||||||
const editorPane = document.getElementById('editor-pane');
|
const editorPane = document.getElementById('editor-pane');
|
||||||
const statusText = document.getElementById('status-text');
|
const statusText = document.getElementById('status-text');
|
||||||
const wordCount = document.getElementById('word-count');
|
const wordCount = document.getElementById('word-count');
|
||||||
|
const lineNumbers = document.getElementById('line-numbers');
|
||||||
|
const findDialog = document.getElementById('find-dialog');
|
||||||
|
const findInput = document.getElementById('find-input');
|
||||||
|
const replaceInput = document.getElementById('replace-input');
|
||||||
|
const findCount = document.getElementById('find-count');
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let isPreviewVisible = true;
|
let isPreviewVisible = true;
|
||||||
let currentContent = '';
|
let currentContent = '';
|
||||||
let isDirty = false;
|
let isDirty = false;
|
||||||
|
let showLineNumbers = false;
|
||||||
|
let findMatches = [];
|
||||||
|
let currentMatchIndex = -1;
|
||||||
|
let undoStack = [];
|
||||||
|
let redoStack = [];
|
||||||
|
let maxUndoSize = 50;
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -61,8 +72,21 @@ document.getElementById('btn-code').addEventListener('click', () => insertMarkdo
|
|||||||
document.getElementById('btn-list').addEventListener('click', () => insertMarkdown('- ', ''));
|
document.getElementById('btn-list').addEventListener('click', () => insertMarkdown('- ', ''));
|
||||||
document.getElementById('btn-quote').addEventListener('click', () => insertMarkdown('> ', ''));
|
document.getElementById('btn-quote').addEventListener('click', () => insertMarkdown('> ', ''));
|
||||||
document.getElementById('btn-table').addEventListener('click', insertTable);
|
document.getElementById('btn-table').addEventListener('click', insertTable);
|
||||||
|
document.getElementById('btn-find').addEventListener('click', toggleFindDialog);
|
||||||
|
document.getElementById('btn-line-numbers').addEventListener('click', toggleLineNumbers);
|
||||||
document.getElementById('btn-preview-toggle').addEventListener('click', togglePreview);
|
document.getElementById('btn-preview-toggle').addEventListener('click', togglePreview);
|
||||||
|
|
||||||
|
// Find dialog handlers
|
||||||
|
document.getElementById('btn-find-close').addEventListener('click', closeFindDialog);
|
||||||
|
document.getElementById('btn-find-next').addEventListener('click', findNext);
|
||||||
|
document.getElementById('btn-find-prev').addEventListener('click', findPrev);
|
||||||
|
document.getElementById('btn-replace').addEventListener('click', replaceOne);
|
||||||
|
document.getElementById('btn-replace-all').addEventListener('click', replaceAll);
|
||||||
|
findInput.addEventListener('input', performFind);
|
||||||
|
replaceInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') replaceOne();
|
||||||
|
});
|
||||||
|
|
||||||
// Table insertion function
|
// Table insertion function
|
||||||
function insertTable() {
|
function insertTable() {
|
||||||
const rows = prompt('Number of rows:', '3');
|
const rows = prompt('Number of rows:', '3');
|
||||||
@@ -251,21 +275,318 @@ ipcRenderer.on('get-content-for-spreadsheet', (event, format) => {
|
|||||||
ipcRenderer.send('export-spreadsheet', { content: editor.value, format });
|
ipcRenderer.send('export-spreadsheet', { content: editor.value, format });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('toggle-find', () => {
|
||||||
|
toggleFindDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced editor input handler with undo/redo and auto-indentation
|
||||||
|
editor.addEventListener('input', (e) => {
|
||||||
|
// Save state for undo
|
||||||
|
if (e.inputType !== 'historyUndo' && e.inputType !== 'historyRedo') {
|
||||||
|
pushUndo();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLineNumbers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push current state to undo stack
|
||||||
|
function pushUndo() {
|
||||||
|
if (undoStack.length >= maxUndoSize) {
|
||||||
|
undoStack.shift();
|
||||||
|
}
|
||||||
|
undoStack.push({
|
||||||
|
content: currentContent,
|
||||||
|
selectionStart: editor.selectionStart,
|
||||||
|
selectionEnd: editor.selectionEnd
|
||||||
|
});
|
||||||
|
redoStack = []; // Clear redo stack when new changes are made
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo function
|
||||||
|
function undo() {
|
||||||
|
if (undoStack.length > 1) {
|
||||||
|
redoStack.push(undoStack.pop());
|
||||||
|
const state = undoStack[undoStack.length - 1];
|
||||||
|
editor.value = state.content;
|
||||||
|
editor.selectionStart = state.selectionStart;
|
||||||
|
editor.selectionEnd = state.selectionEnd;
|
||||||
|
currentContent = state.content;
|
||||||
|
updatePreview();
|
||||||
|
updateWordCount();
|
||||||
|
updateLineNumbers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redo function
|
||||||
|
function redo() {
|
||||||
|
if (redoStack.length > 0) {
|
||||||
|
const state = redoStack.pop();
|
||||||
|
undoStack.push(state);
|
||||||
|
editor.value = state.content;
|
||||||
|
editor.selectionStart = state.selectionStart;
|
||||||
|
editor.selectionEnd = state.selectionEnd;
|
||||||
|
currentContent = state.content;
|
||||||
|
updatePreview();
|
||||||
|
updateWordCount();
|
||||||
|
updateLineNumbers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find & Replace functionality
|
||||||
|
function toggleFindDialog() {
|
||||||
|
if (findDialog.classList.contains('hidden')) {
|
||||||
|
findDialog.classList.remove('hidden');
|
||||||
|
findInput.focus();
|
||||||
|
if (editor.selectionStart !== editor.selectionEnd) {
|
||||||
|
findInput.value = editor.value.substring(editor.selectionStart, editor.selectionEnd);
|
||||||
|
performFind();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
closeFindDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFindDialog() {
|
||||||
|
findDialog.classList.add('hidden');
|
||||||
|
clearHighlights();
|
||||||
|
editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function performFind() {
|
||||||
|
const searchText = findInput.value;
|
||||||
|
clearHighlights();
|
||||||
|
findMatches = [];
|
||||||
|
currentMatchIndex = -1;
|
||||||
|
|
||||||
|
if (!searchText) {
|
||||||
|
findCount.textContent = '0 matches';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = editor.value;
|
||||||
|
let index = 0;
|
||||||
|
while ((index = content.indexOf(searchText, index)) !== -1) {
|
||||||
|
findMatches.push(index);
|
||||||
|
index += searchText.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
findCount.textContent = `${findMatches.length} matches`;
|
||||||
|
|
||||||
|
if (findMatches.length > 0) {
|
||||||
|
currentMatchIndex = 0;
|
||||||
|
highlightMatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNext() {
|
||||||
|
if (findMatches.length === 0) return;
|
||||||
|
currentMatchIndex = (currentMatchIndex + 1) % findMatches.length;
|
||||||
|
highlightMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPrev() {
|
||||||
|
if (findMatches.length === 0) return;
|
||||||
|
currentMatchIndex = currentMatchIndex === 0 ? findMatches.length - 1 : currentMatchIndex - 1;
|
||||||
|
highlightMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightMatch() {
|
||||||
|
if (currentMatchIndex === -1 || !findMatches[currentMatchIndex]) return;
|
||||||
|
|
||||||
|
const matchStart = findMatches[currentMatchIndex];
|
||||||
|
const matchEnd = matchStart + findInput.value.length;
|
||||||
|
|
||||||
|
editor.selectionStart = matchStart;
|
||||||
|
editor.selectionEnd = matchEnd;
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
|
// Scroll to match
|
||||||
|
const lineHeight = 20; // Approximate line height
|
||||||
|
const lineNumber = editor.value.substring(0, matchStart).split('\n').length;
|
||||||
|
editor.scrollTop = Math.max(0, (lineNumber - 10) * lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceOne() {
|
||||||
|
if (currentMatchIndex === -1 || !findMatches[currentMatchIndex]) return;
|
||||||
|
|
||||||
|
const searchText = findInput.value;
|
||||||
|
const replaceText = replaceInput.value;
|
||||||
|
const matchStart = findMatches[currentMatchIndex];
|
||||||
|
const matchEnd = matchStart + searchText.length;
|
||||||
|
|
||||||
|
editor.value = editor.value.substring(0, matchStart) +
|
||||||
|
replaceText +
|
||||||
|
editor.value.substring(matchEnd);
|
||||||
|
|
||||||
|
editor.selectionStart = matchStart;
|
||||||
|
editor.selectionEnd = matchStart + replaceText.length;
|
||||||
|
|
||||||
|
// Trigger input event
|
||||||
|
editor.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Refresh find
|
||||||
|
setTimeout(() => performFind(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceAll() {
|
||||||
|
const searchText = findInput.value;
|
||||||
|
const replaceText = replaceInput.value;
|
||||||
|
|
||||||
|
if (!searchText) return;
|
||||||
|
|
||||||
|
let content = editor.value;
|
||||||
|
let replacements = 0;
|
||||||
|
|
||||||
|
while (content.includes(searchText)) {
|
||||||
|
content = content.replace(searchText, replaceText);
|
||||||
|
replacements++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacements > 0) {
|
||||||
|
editor.value = content;
|
||||||
|
editor.dispatchEvent(new Event('input'));
|
||||||
|
updateStatus(`Replaced ${replacements} occurrences`);
|
||||||
|
performFind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHighlights() {
|
||||||
|
// Clear any highlights (in a real implementation, you'd remove highlight spans)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line Numbers functionality
|
||||||
|
function toggleLineNumbers() {
|
||||||
|
showLineNumbers = !showLineNumbers;
|
||||||
|
if (showLineNumbers) {
|
||||||
|
lineNumbers.classList.remove('hidden');
|
||||||
|
updateLineNumbers();
|
||||||
|
} else {
|
||||||
|
lineNumbers.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLineNumbers() {
|
||||||
|
if (!showLineNumbers) return;
|
||||||
|
|
||||||
|
const lines = editor.value.split('\n');
|
||||||
|
const lineNumbersHtml = lines.map((_, index) =>
|
||||||
|
`<span class="line-number">${index + 1}</span>`
|
||||||
|
).join('');
|
||||||
|
lineNumbers.innerHTML = lineNumbersHtml;
|
||||||
|
|
||||||
|
// Sync scroll
|
||||||
|
lineNumbers.scrollTop = editor.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync line numbers scroll with editor
|
||||||
|
editor.addEventListener('scroll', () => {
|
||||||
|
if (showLineNumbers) {
|
||||||
|
lineNumbers.scrollTop = editor.scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-indentation for lists
|
||||||
|
function handleEnterKey(e) {
|
||||||
|
const cursorPos = editor.selectionStart;
|
||||||
|
const beforeCursor = editor.value.substring(0, cursorPos);
|
||||||
|
const lines = beforeCursor.split('\n');
|
||||||
|
const currentLine = lines[lines.length - 1];
|
||||||
|
|
||||||
|
// Check for list patterns
|
||||||
|
const listMatch = currentLine.match(/^(\s*)([-*+]|\d+\.)\s/);
|
||||||
|
if (listMatch) {
|
||||||
|
e.preventDefault();
|
||||||
|
const indent = listMatch[1];
|
||||||
|
const marker = listMatch[2];
|
||||||
|
|
||||||
|
// If current line is just the marker, remove it
|
||||||
|
if (currentLine.trim() === marker) {
|
||||||
|
const lineStart = beforeCursor.lastIndexOf('\n') + 1;
|
||||||
|
editor.value = editor.value.substring(0, lineStart) +
|
||||||
|
editor.value.substring(cursorPos);
|
||||||
|
editor.selectionStart = editor.selectionEnd = lineStart;
|
||||||
|
} else {
|
||||||
|
// Continue the list
|
||||||
|
let newMarker = marker;
|
||||||
|
if (/^\d+\./.test(marker)) {
|
||||||
|
const num = parseInt(marker) + 1;
|
||||||
|
newMarker = num + '.';
|
||||||
|
}
|
||||||
|
const insertion = '\n' + indent + newMarker + ' ';
|
||||||
|
editor.value = editor.value.substring(0, cursorPos) +
|
||||||
|
insertion +
|
||||||
|
editor.value.substring(cursorPos);
|
||||||
|
editor.selectionStart = editor.selectionEnd = cursorPos + insertion.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Ctrl/Cmd + F for find
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleFindDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd + Z for undo
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl/Cmd + Shift + Z for redo
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
redo();
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl/Cmd + Enter to toggle preview
|
// Ctrl/Cmd + Enter to toggle preview
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
togglePreview();
|
togglePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab key in editor
|
// Enhanced Tab handling in editor
|
||||||
if (e.key === 'Tab' && e.target === editor) {
|
if (e.key === 'Tab' && e.target === editor) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const start = editor.selectionStart;
|
const start = editor.selectionStart;
|
||||||
const end = editor.selectionEnd;
|
const end = editor.selectionEnd;
|
||||||
|
|
||||||
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
if (start === end) {
|
||||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
// Simple tab insertion
|
||||||
|
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
||||||
|
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||||
|
} else {
|
||||||
|
// Indent/outdent selected lines
|
||||||
|
const beforeSelection = editor.value.substring(0, start);
|
||||||
|
const selection = editor.value.substring(start, end);
|
||||||
|
const afterSelection = editor.value.substring(end);
|
||||||
|
|
||||||
|
const lines = selection.split('\n');
|
||||||
|
const indentedLines = e.shiftKey ?
|
||||||
|
lines.map(line => line.replace(/^ /, '')) : // Outdent
|
||||||
|
lines.map(line => ' ' + line); // Indent
|
||||||
|
|
||||||
|
const newSelection = indentedLines.join('\n');
|
||||||
|
editor.value = beforeSelection + newSelection + afterSelection;
|
||||||
|
|
||||||
|
editor.selectionStart = start;
|
||||||
|
editor.selectionEnd = start + newSelection.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter key handling for auto-indentation
|
||||||
|
if (e.key === 'Enter' && e.target === editor) {
|
||||||
|
handleEnterKey(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape to close find dialog
|
||||||
|
if (e.key === 'Escape' && !findDialog.classList.contains('hidden')) {
|
||||||
|
closeFindDialog();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+136
@@ -459,4 +459,140 @@ body.theme-github .status-bar {
|
|||||||
|
|
||||||
#editor-pane.full-width {
|
#editor-pane.full-width {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find & Replace Dialog */
|
||||||
|
.find-dialog {
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
right: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 400px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-dialog.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-controls input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-controls button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-controls button:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor Wrapper with Line Numbers */
|
||||||
|
.editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
min-width: 40px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #666;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers .line-number {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust editor when line numbers are shown */
|
||||||
|
.editor-wrapper #editor {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme support for find dialog */
|
||||||
|
body.theme-dark .find-dialog {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .find-controls input {
|
||||||
|
background: #333;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .find-controls button {
|
||||||
|
background: #404040;
|
||||||
|
border-color: #555;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .find-controls button:hover {
|
||||||
|
background: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .line-numbers {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-right-color: #555;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional theme support for line numbers */
|
||||||
|
body.theme-solarized .line-numbers {
|
||||||
|
background: #fdf6e3;
|
||||||
|
border-right-color: #eee8d5;
|
||||||
|
color: #93a1a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .line-numbers {
|
||||||
|
background: #2f2f2f;
|
||||||
|
border-right-color: #555;
|
||||||
|
color: #75715e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .line-numbers {
|
||||||
|
background: #fafbfc;
|
||||||
|
border-right-color: #e1e4e8;
|
||||||
|
color: #586069;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user