mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat: replace textarea with CodeMirror 6 editor
This commit is contained in:
+4
-2
@@ -1297,12 +1297,14 @@
|
|||||||
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
||||||
<div id="editor-pane-1" class="pane">
|
<div id="editor-pane-1" class="pane">
|
||||||
<div class="editor-wrapper">
|
<div class="editor-wrapper">
|
||||||
<div id="line-numbers-1" class="line-numbers hidden"></div>
|
<div id="editor-cm-1" class="codemirror-container"></div>
|
||||||
<textarea id="editor-1" class="editor-textarea"></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div>
|
<div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div>
|
||||||
<div id="preview-pane-1" class="pane">
|
<div id="preview-pane-1" class="pane">
|
||||||
|
<div class="preview-header">
|
||||||
|
<button class="preview-popout-btn" id="preview-popout-1" title="Pop out preview in new window">⬜</button>
|
||||||
|
</div>
|
||||||
<div id="preview-1" class="preview-content"></div>
|
<div id="preview-1" class="preview-content"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+202
-295
@@ -8,6 +8,7 @@ const marked = require('marked');
|
|||||||
const { markedHighlight } = require('marked-highlight');
|
const { markedHighlight } = require('marked-highlight');
|
||||||
const DOMPurify = require('dompurify');
|
const DOMPurify = require('dompurify');
|
||||||
const hljs = require('highlight.js');
|
const hljs = require('highlight.js');
|
||||||
|
const { createEditor } = require('./editor/codemirror-setup');
|
||||||
|
|
||||||
// Configure marked with highlight extension
|
// Configure marked with highlight extension
|
||||||
marked.use(markedHighlight({
|
marked.use(markedHighlight({
|
||||||
@@ -47,8 +48,7 @@ class TabManager {
|
|||||||
content: '',
|
content: '',
|
||||||
filePath: null,
|
filePath: null,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
undoStack: [],
|
editorView: null,
|
||||||
redoStack: [],
|
|
||||||
findMatches: [],
|
findMatches: [],
|
||||||
currentMatchIndex: -1
|
currentMatchIndex: -1
|
||||||
});
|
});
|
||||||
@@ -114,8 +114,7 @@ class TabManager {
|
|||||||
content: '',
|
content: '',
|
||||||
filePath: null,
|
filePath: null,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
undoStack: [],
|
editorView: null,
|
||||||
redoStack: [],
|
|
||||||
findMatches: [],
|
findMatches: [],
|
||||||
currentMatchIndex: -1
|
currentMatchIndex: -1
|
||||||
};
|
};
|
||||||
@@ -137,8 +136,7 @@ class TabManager {
|
|||||||
tabContent.innerHTML = `
|
tabContent.innerHTML = `
|
||||||
<div id="editor-pane-${tab.id}" class="pane">
|
<div id="editor-pane-${tab.id}" class="pane">
|
||||||
<div class="editor-wrapper">
|
<div class="editor-wrapper">
|
||||||
<div id="line-numbers-${tab.id}" class="line-numbers hidden"></div>
|
<div id="editor-cm-${tab.id}" class="codemirror-container"></div>
|
||||||
<textarea id="editor-${tab.id}" class="editor-textarea"></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-resizer" id="pane-resizer-${tab.id}" title="Drag to resize"></div>
|
<div class="pane-resizer" id="pane-resizer-${tab.id}" title="Drag to resize"></div>
|
||||||
@@ -152,21 +150,21 @@ class TabManager {
|
|||||||
|
|
||||||
document.querySelector('.editor-container').appendChild(tabContent);
|
document.querySelector('.editor-container').appendChild(tabContent);
|
||||||
|
|
||||||
// Directly attach input listener to the new editor
|
// Initialize CodeMirror editor
|
||||||
const editor = document.getElementById(`editor-${tab.id}`);
|
const editorContainer = document.getElementById(`editor-cm-${tab.id}`);
|
||||||
if (editor) {
|
if (editorContainer) {
|
||||||
editor.addEventListener('input', () => {
|
const isDark = document.body.className.includes('dark');
|
||||||
this.handleEditorInput(tab.id);
|
tab.editorView = createEditor(editorContainer, {
|
||||||
});
|
content: tab.content,
|
||||||
|
onChange: (newContent) => {
|
||||||
// Add scroll listener for line number sync
|
tab.content = newContent;
|
||||||
editor.addEventListener('scroll', () => {
|
tab.isDirty = true;
|
||||||
if (this.showLineNumbers && this.activeTabId === tab.id) {
|
this.updatePreview(tab.id);
|
||||||
const lineNumbers = document.getElementById(`line-numbers-${tab.id}`);
|
this.updateWordCount();
|
||||||
if (lineNumbers) {
|
this.updateTabBar();
|
||||||
lineNumbers.scrollTop = editor.scrollTop;
|
},
|
||||||
}
|
isDark,
|
||||||
}
|
showLineNumbers: this.showLineNumbers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,25 +198,30 @@ class TabManager {
|
|||||||
|
|
||||||
closeTab(tabId) {
|
closeTab(tabId) {
|
||||||
if (this.tabs.size === 1) return; // Don't close the last tab
|
if (this.tabs.size === 1) return; // Don't close the last tab
|
||||||
|
|
||||||
const tab = this.tabs.get(tabId);
|
const tab = this.tabs.get(tabId);
|
||||||
if (tab.isDirty) {
|
if (tab.isDirty) {
|
||||||
// Show confirmation dialog for unsaved changes
|
// Show confirmation dialog for unsaved changes
|
||||||
const result = confirm('You have unsaved changes. Do you want to close this tab without saving?');
|
const result = confirm('You have unsaved changes. Do you want to close this tab without saving?');
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroy CodeMirror view
|
||||||
|
if (tab?.editorView) {
|
||||||
|
tab.editorView.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
// Remove tab elements
|
// Remove tab elements
|
||||||
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
|
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
|
||||||
const tabContent = document.getElementById(`tab-content-${tabId}`);
|
const tabContent = document.getElementById(`tab-content-${tabId}`);
|
||||||
|
|
||||||
if (tabElement?.classList.contains('tab')) {
|
if (tabElement?.classList.contains('tab')) {
|
||||||
tabElement.remove();
|
tabElement.remove();
|
||||||
}
|
}
|
||||||
if (tabContent) {
|
if (tabContent) {
|
||||||
tabContent.remove();
|
tabContent.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tabs.delete(tabId);
|
this.tabs.delete(tabId);
|
||||||
|
|
||||||
// Switch to another tab if this was active
|
// Switch to another tab if this was active
|
||||||
@@ -281,10 +284,9 @@ class TabManager {
|
|||||||
saveCurrentTabState() {
|
saveCurrentTabState() {
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
if (tab.editorView) {
|
||||||
if (editor) {
|
tab.content = tab.editorView.state.doc.toString();
|
||||||
tab.content = editor.value;
|
|
||||||
tab.isDirty = tab.content !== (tab.originalContent || '');
|
tab.isDirty = tab.content !== (tab.originalContent || '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,19 +295,17 @@ class TabManager {
|
|||||||
const tab = this.tabs.get(tabId);
|
const tab = this.tabs.get(tabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const editor = document.getElementById(`editor-${tabId}`);
|
if (tab.editorView) {
|
||||||
|
this.setEditorContent(tabId, tab.content);
|
||||||
if (editor) {
|
|
||||||
editor.value = tab.content;
|
|
||||||
this.updatePreview(tabId);
|
this.updatePreview(tabId);
|
||||||
this.updateWordCount();
|
this.updateWordCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focusActiveEditor() {
|
focusActiveEditor() {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (editor) {
|
if (tab?.editorView) {
|
||||||
editor.focus();
|
tab.editorView.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,23 +403,9 @@ class TabManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLineNumbers() {
|
updateLineNumbers() {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
// CodeMirror handles line numbers natively — this is now a no-op.
|
||||||
const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`);
|
// Line number visibility is controlled via the showLineNumbers option
|
||||||
|
// passed to createEditor().
|
||||||
if (!editor || !lineNumbers) return;
|
|
||||||
|
|
||||||
if (this.showLineNumbers) {
|
|
||||||
const lines = editor.value.split('\n');
|
|
||||||
lineNumbers.innerHTML = lines.map((_, i) =>
|
|
||||||
`<div class="line-number">${i + 1}</div>`
|
|
||||||
).join('');
|
|
||||||
lineNumbers.classList.remove('hidden');
|
|
||||||
|
|
||||||
// Sync scroll position
|
|
||||||
lineNumbers.scrollTop = editor.scrollTop;
|
|
||||||
} else {
|
|
||||||
lineNumbers.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateWordCount() {
|
updateWordCount() {
|
||||||
@@ -455,82 +441,45 @@ class TabManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupEditorEvents() {
|
setupEditorEvents() {
|
||||||
// Set up editor events using event delegation on the container
|
// CodeMirror handles editor events via the onChange callback
|
||||||
const editorContainer = document.querySelector('.editor-container');
|
// passed to createEditor(). No additional event delegation needed.
|
||||||
if (editorContainer) {
|
|
||||||
editorContainer.addEventListener('input', (e) => {
|
|
||||||
if (e.target.classList.contains('editor-textarea')) {
|
|
||||||
const tabId = parseInt(e.target.id.split('-')[1]);
|
|
||||||
this.handleEditorInput(tabId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editorContainer.addEventListener('scroll', (e) => {
|
|
||||||
if (e.target.classList.contains('editor-textarea')) {
|
|
||||||
this.updateLineNumbers();
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditorInput(tabId) {
|
handleEditorInput(tabId) {
|
||||||
|
// CodeMirror's onChange callback handles content syncing.
|
||||||
|
// This method is kept for any remaining callers but is largely a no-op now.
|
||||||
const tab = this.tabs.get(tabId);
|
const tab = this.tabs.get(tabId);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
const editor = document.getElementById(`editor-${tabId}`);
|
if (tab.editorView) {
|
||||||
if (!editor) return;
|
tab.content = tab.editorView.state.doc.toString();
|
||||||
|
}
|
||||||
tab.content = editor.value;
|
|
||||||
tab.isDirty = true;
|
tab.isDirty = true;
|
||||||
|
|
||||||
this.updatePreview(tabId);
|
this.updatePreview(tabId);
|
||||||
this.updateWordCount();
|
this.updateWordCount();
|
||||||
this.updateLineNumbers();
|
|
||||||
this.updateTabBar();
|
this.updateTabBar();
|
||||||
|
|
||||||
// Add to undo stack
|
|
||||||
this.pushUndoState(tabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pushUndoState(tabId) {
|
pushUndoState(tabId) {
|
||||||
const tab = this.tabs.get(tabId);
|
// CodeMirror has built-in history — this is now a no-op.
|
||||||
if (!tab) return;
|
// Kept as a stub for any remaining callers.
|
||||||
|
|
||||||
tab.undoStack.push(tab.content);
|
|
||||||
if (tab.undoStack.length > 50) {
|
|
||||||
tab.undoStack.shift();
|
|
||||||
}
|
|
||||||
tab.redoStack = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
|
// CodeMirror handles undo via its built-in history extension.
|
||||||
|
// This stub is kept for the IPC handler; Task 9 will wire it properly.
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!tab || tab.undoStack.length === 0) return;
|
if (!tab?.editorView) return;
|
||||||
|
// CodeMirror's Ctrl+Z keybinding handles undo automatically.
|
||||||
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() {
|
redo() {
|
||||||
|
// CodeMirror handles redo via its built-in history extension.
|
||||||
|
// This stub is kept for the IPC handler; Task 9 will wire it properly.
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!tab || tab.redoStack.length === 0) return;
|
if (!tab?.editorView) return;
|
||||||
|
// CodeMirror's Ctrl+Shift+Z / Ctrl+Y keybinding handles redo automatically.
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-save functionality
|
// Auto-save functionality
|
||||||
@@ -681,103 +630,64 @@ class TabManager {
|
|||||||
|
|
||||||
// Helper function to wrap selected text
|
// Helper function to wrap selected text
|
||||||
wrapSelection(before, after) {
|
wrapSelection(before, after) {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!editor) return;
|
if (!tab?.editorView) return;
|
||||||
|
|
||||||
const start = editor.selectionStart;
|
const view = tab.editorView;
|
||||||
const end = editor.selectionEnd;
|
const { from, to } = view.state.selection.main;
|
||||||
const selectedText = editor.value.substring(start, end);
|
const selectedText = view.state.sliceDoc(from, to);
|
||||||
const replacement = before + (selectedText || 'text') + after;
|
const replacement = before + (selectedText || 'text') + after;
|
||||||
|
|
||||||
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: replacement }
|
||||||
// Update cursor position
|
});
|
||||||
const newCursorPos = selectedText ? start + replacement.length : start + before.length;
|
view.focus();
|
||||||
editor.selectionStart = editor.selectionEnd = newCursorPos;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
|
||||||
this.handleEditorInput(this.activeTabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to insert text at the start of current line
|
// Helper function to insert text at the start of current line
|
||||||
insertAtLineStart(prefix) {
|
insertAtLineStart(prefix) {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!editor) return;
|
if (!tab?.editorView) return;
|
||||||
|
|
||||||
const start = editor.selectionStart;
|
const view = tab.editorView;
|
||||||
const text = editor.value;
|
const pos = view.state.selection.main.head;
|
||||||
|
const line = view.state.doc.lineAt(pos);
|
||||||
|
|
||||||
// Find the start of the current line
|
view.dispatch({
|
||||||
let lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
changes: { from: line.from, insert: prefix }
|
||||||
|
});
|
||||||
// Insert the prefix
|
view.focus();
|
||||||
editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + prefix.length;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
|
||||||
this.handleEditorInput(this.activeTabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert a markdown table
|
// Insert a markdown table
|
||||||
insertTable() {
|
insertTable() {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
const table = '\n| Column 1 | Column 2 | Column 3 |\n' +
|
const table = '\n| Column 1 | Column 2 | Column 3 |\n' +
|
||||||
'|----------|----------|----------|\n' +
|
'|----------|----------|----------|\n' +
|
||||||
'| Cell 1 | Cell 2 | Cell 3 |\n' +
|
'| Cell 1 | Cell 2 | Cell 3 |\n' +
|
||||||
'| Cell 4 | Cell 5 | Cell 6 |\n';
|
'| Cell 4 | Cell 5 | Cell 6 |\n';
|
||||||
|
|
||||||
const start = editor.selectionStart;
|
this.insertAtCursor(table);
|
||||||
editor.value = editor.value.substring(0, start) + table + editor.value.substring(start);
|
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + table.length;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
|
||||||
this.handleEditorInput(this.activeTabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert a code block
|
// Insert a code block
|
||||||
insertCodeBlock() {
|
insertCodeBlock() {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
if (!editor) return;
|
if (!tab?.editorView) return;
|
||||||
|
|
||||||
const selectedText = editor.value.substring(editor.selectionStart, editor.selectionEnd);
|
const view = tab.editorView;
|
||||||
|
const { from, to } = view.state.selection.main;
|
||||||
|
const selectedText = view.state.sliceDoc(from, to);
|
||||||
const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n';
|
const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n';
|
||||||
|
|
||||||
const start = editor.selectionStart;
|
view.dispatch({
|
||||||
editor.value = editor.value.substring(0, start) + codeBlock + editor.value.substring(editor.selectionEnd);
|
changes: { from, to, insert: codeBlock }
|
||||||
|
});
|
||||||
// Update cursor position
|
view.focus();
|
||||||
editor.selectionStart = editor.selectionEnd = start + codeBlock.length;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
|
||||||
this.handleEditorInput(this.activeTabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert a horizontal rule
|
// Insert a horizontal rule
|
||||||
insertHorizontalRule() {
|
insertHorizontalRule() {
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
this.insertAtCursor('\n\n---\n\n');
|
||||||
if (!editor) return;
|
|
||||||
|
|
||||||
const hr = '\n\n---\n\n';
|
|
||||||
const start = editor.selectionStart;
|
|
||||||
|
|
||||||
editor.value = editor.value.substring(0, start) + hr + editor.value.substring(start);
|
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + hr.length;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
|
||||||
this.handleEditorInput(this.activeTabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFindEvents() {
|
setupFindEvents() {
|
||||||
@@ -848,9 +758,8 @@ class TabManager {
|
|||||||
performFind() {
|
performFind() {
|
||||||
const findText = document.getElementById('find-input').value;
|
const findText = document.getElementById('find-input').value;
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
|
||||||
|
|
||||||
if (!findText || !tab || !editor) {
|
if (!findText || !tab) {
|
||||||
this.clearFindHighlights();
|
this.clearFindHighlights();
|
||||||
const findCount = document.getElementById('find-count');
|
const findCount = document.getElementById('find-count');
|
||||||
if (findCount) {
|
if (findCount) {
|
||||||
@@ -859,7 +768,7 @@ class TabManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = editor.value;
|
const content = this.getEditorContent();
|
||||||
const matches = [];
|
const matches = [];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
@@ -905,40 +814,28 @@ class TabManager {
|
|||||||
|
|
||||||
highlightMatch(matchIndex) {
|
highlightMatch(matchIndex) {
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
|
||||||
const findText = document.getElementById('find-input').value;
|
const findText = document.getElementById('find-input').value;
|
||||||
|
|
||||||
if (!tab || !editor || matchIndex < 0 || matchIndex >= tab.findMatches.length) return;
|
if (!tab || !tab.editorView || matchIndex < 0 || matchIndex >= tab.findMatches.length) return;
|
||||||
|
|
||||||
const position = tab.findMatches[matchIndex];
|
const position = tab.findMatches[matchIndex];
|
||||||
|
const view = tab.editorView;
|
||||||
|
|
||||||
// Select the match WITHOUT focusing (to keep focus on find input)
|
// Select the match in CodeMirror
|
||||||
editor.setSelectionRange(position, position + findText.length);
|
view.dispatch({
|
||||||
|
selection: { anchor: position, head: position + findText.length },
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Make the selection visible by briefly focusing and then restoring focus
|
// Restore focus to find input
|
||||||
const findInput = document.getElementById('find-input');
|
const findInput = document.getElementById('find-input');
|
||||||
const hadFocus = document.activeElement === findInput;
|
if (findInput) {
|
||||||
|
|
||||||
// Temporarily focus editor to make selection visible
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Restore focus to find input if it had focus
|
|
||||||
if (hadFocus) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
findInput.focus();
|
findInput.focus();
|
||||||
// Restore cursor position in find input
|
|
||||||
findInput.setSelectionRange(findInput.value.length, findInput.value.length);
|
findInput.setSelectionRange(findInput.value.length, findInput.value.length);
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll into view
|
|
||||||
const lineHeight = 20; // Approximate line height
|
|
||||||
const charPosition = position;
|
|
||||||
const numLines = editor.value.substring(0, charPosition).split('\n').length;
|
|
||||||
const scrollPosition = (numLines - 5) * lineHeight; // Show match 5 lines from top
|
|
||||||
|
|
||||||
editor.scrollTop = Math.max(0, scrollPosition);
|
|
||||||
|
|
||||||
// Update match counter
|
// Update match counter
|
||||||
const findCount = document.getElementById('find-count');
|
const findCount = document.getElementById('find-count');
|
||||||
if (findCount) {
|
if (findCount) {
|
||||||
@@ -948,18 +845,19 @@ class TabManager {
|
|||||||
|
|
||||||
replaceOne() {
|
replaceOne() {
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
|
||||||
const findText = document.getElementById('find-input').value;
|
const findText = document.getElementById('find-input').value;
|
||||||
const replaceText = document.getElementById('replace-input').value;
|
const replaceText = document.getElementById('replace-input').value;
|
||||||
|
|
||||||
if (!tab || !editor || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return;
|
if (!tab || !tab.editorView || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return;
|
||||||
|
|
||||||
const position = tab.findMatches[tab.currentMatchIndex];
|
const position = tab.findMatches[tab.currentMatchIndex];
|
||||||
const before = editor.value.substring(0, position);
|
const view = tab.editorView;
|
||||||
const after = editor.value.substring(position + findText.length);
|
|
||||||
|
|
||||||
editor.value = before + replaceText + after;
|
view.dispatch({
|
||||||
tab.content = editor.value;
|
changes: { from: position, to: position + findText.length, insert: replaceText }
|
||||||
|
});
|
||||||
|
|
||||||
|
tab.content = view.state.doc.toString();
|
||||||
tab.isDirty = true;
|
tab.isDirty = true;
|
||||||
|
|
||||||
this.updatePreview(this.activeTabId);
|
this.updatePreview(this.activeTabId);
|
||||||
@@ -972,18 +870,16 @@ class TabManager {
|
|||||||
|
|
||||||
replaceAll() {
|
replaceAll() {
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
|
||||||
const findText = document.getElementById('find-input').value;
|
const findText = document.getElementById('find-input').value;
|
||||||
const replaceText = document.getElementById('replace-input').value;
|
const replaceText = document.getElementById('replace-input').value;
|
||||||
|
|
||||||
if (!tab || !editor || !findText) return;
|
if (!tab || !tab.editorView || !findText) return;
|
||||||
|
|
||||||
// Simple replace all
|
const content = this.getEditorContent();
|
||||||
const newContent = editor.value.split(findText).join(replaceText);
|
const newContent = content.split(findText).join(replaceText);
|
||||||
const replacedCount = tab.findMatches.length;
|
const replacedCount = tab.findMatches.length;
|
||||||
|
|
||||||
editor.value = newContent;
|
this.setEditorContent(this.activeTabId, newContent);
|
||||||
tab.content = newContent;
|
|
||||||
tab.isDirty = true;
|
tab.isDirty = true;
|
||||||
|
|
||||||
this.updatePreview(this.activeTabId);
|
this.updatePreview(this.activeTabId);
|
||||||
@@ -1023,13 +919,9 @@ class TabManager {
|
|||||||
tab.isDirty = false;
|
tab.isDirty = false;
|
||||||
|
|
||||||
// Update the editor and preview
|
// Update the editor and preview
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
this.setEditorContent(this.activeTabId, content);
|
||||||
if (editor) {
|
this.updatePreview(this.activeTabId);
|
||||||
editor.value = content;
|
this.updateWordCount();
|
||||||
// Update preview after editor is updated
|
|
||||||
this.updatePreview(this.activeTabId);
|
|
||||||
this.updateWordCount();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Create new tab for the file
|
// Create new tab for the file
|
||||||
console.log('Creating new tab for file');
|
console.log('Creating new tab for file');
|
||||||
@@ -1041,15 +933,10 @@ class TabManager {
|
|||||||
tab.originalContent = content;
|
tab.originalContent = content;
|
||||||
tab.isDirty = false;
|
tab.isDirty = false;
|
||||||
|
|
||||||
// Wait a moment for the DOM to update, then set content
|
// Set content in the CodeMirror editor
|
||||||
setTimeout(() => {
|
this.setEditorContent(this.activeTabId, content);
|
||||||
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
this.updatePreview(this.activeTabId);
|
||||||
if (editor) {
|
this.updateWordCount();
|
||||||
editor.value = content;
|
|
||||||
this.updatePreview(this.activeTabId);
|
|
||||||
this.updateWordCount();
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
}
|
||||||
this.startAutoSave();
|
this.startAutoSave();
|
||||||
this.addToRecentFiles(filePath);
|
this.addToRecentFiles(filePath);
|
||||||
@@ -1061,6 +948,58 @@ class TabManager {
|
|||||||
console.log('File opened successfully');
|
console.log('File opened successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get content from active editor
|
||||||
|
getEditorContent(tabId = this.activeTabId) {
|
||||||
|
const tab = this.tabs.get(tabId);
|
||||||
|
if (tab?.editorView) {
|
||||||
|
return tab.editorView.state.doc.toString();
|
||||||
|
}
|
||||||
|
return tab?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content in editor
|
||||||
|
setEditorContent(tabId, content) {
|
||||||
|
const tab = this.tabs.get(tabId);
|
||||||
|
if (tab?.editorView) {
|
||||||
|
tab.editorView.dispatch({
|
||||||
|
changes: { from: 0, to: tab.editorView.state.doc.length, insert: content }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tab.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert text at cursor position
|
||||||
|
insertAtCursor(text) {
|
||||||
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
|
if (!tab?.editorView) return;
|
||||||
|
const view = tab.editorView;
|
||||||
|
const pos = view.state.selection.main.head;
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: pos, insert: text }
|
||||||
|
});
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected text
|
||||||
|
getSelection() {
|
||||||
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
|
if (!tab?.editorView) return '';
|
||||||
|
const state = tab.editorView.state;
|
||||||
|
return state.sliceDoc(state.selection.main.from, state.selection.main.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace selection
|
||||||
|
replaceSelection(text) {
|
||||||
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
|
if (!tab?.editorView) return;
|
||||||
|
const view = tab.editorView;
|
||||||
|
const { from, to } = view.state.selection.main;
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: text }
|
||||||
|
});
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentContent() {
|
getCurrentContent() {
|
||||||
const tab = this.tabs.get(this.activeTabId);
|
const tab = this.tabs.get(this.activeTabId);
|
||||||
return tab ? tab.content : '';
|
return tab ? tab.content : '';
|
||||||
@@ -1078,21 +1017,22 @@ let tabManager;
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
tabManager = new TabManager();
|
tabManager = new TabManager();
|
||||||
|
|
||||||
// Attach input listener to the initial editor (tab 1)
|
// Initialize CodeMirror for the initial tab (tab 1)
|
||||||
const initialEditor = document.getElementById('editor-1');
|
const editorContainer = document.getElementById('editor-cm-1');
|
||||||
if (initialEditor) {
|
if (editorContainer) {
|
||||||
initialEditor.addEventListener('input', () => {
|
const tab = tabManager.tabs.get(1);
|
||||||
tabManager.handleEditorInput(1);
|
const isDark = document.body.className.includes('dark');
|
||||||
});
|
tab.editorView = createEditor(editorContainer, {
|
||||||
|
content: tab.content,
|
||||||
// Add scroll listener for line number sync
|
onChange: (newContent) => {
|
||||||
initialEditor.addEventListener('scroll', () => {
|
tab.content = newContent;
|
||||||
if (tabManager.showLineNumbers && tabManager.activeTabId === 1) {
|
tab.isDirty = true;
|
||||||
const lineNumbers = document.getElementById('line-numbers-1');
|
tabManager.updatePreview(tab.id);
|
||||||
if (lineNumbers) {
|
tabManager.updateWordCount();
|
||||||
lineNumbers.scrollTop = initialEditor.scrollTop;
|
tabManager.updateTabBar();
|
||||||
}
|
},
|
||||||
}
|
isDark,
|
||||||
|
showLineNumbers: tabManager.showLineNumbers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,17 +1133,17 @@ ipcRenderer.on('redo', () => {
|
|||||||
let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15;
|
let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15;
|
||||||
|
|
||||||
function updateFontSizes(size) {
|
function updateFontSizes(size) {
|
||||||
const editors = document.querySelectorAll('#editor, .editor-textarea');
|
const editors = document.querySelectorAll('.cm-editor');
|
||||||
const previews = document.querySelectorAll('#preview, .preview-content');
|
const previews = document.querySelectorAll('#preview, .preview-content');
|
||||||
|
|
||||||
editors.forEach(editor => {
|
editors.forEach(editor => {
|
||||||
editor.style.fontSize = `${size}px`;
|
editor.style.fontSize = `${size}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
previews.forEach(preview => {
|
previews.forEach(preview => {
|
||||||
preview.style.fontSize = `${size}px`;
|
preview.style.fontSize = `${size}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem('fontSize', size);
|
localStorage.setItem('fontSize', size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3341,31 +3281,19 @@ function insertGeneratedTable() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active editor
|
// Insert table using CodeMirror
|
||||||
const activeTabId = tabManager ? tabManager.activeTabId : 1;
|
if (!tabManager) {
|
||||||
const editor = document.getElementById(`editor-${activeTabId}`);
|
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
alert('No active editor found');
|
alert('No active editor found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert table at cursor position
|
|
||||||
const start = editor.selectionStart;
|
|
||||||
const before = editor.value.substring(0, start);
|
|
||||||
const after = editor.value.substring(editor.selectionEnd);
|
|
||||||
|
|
||||||
// Add newlines for spacing
|
// Add newlines for spacing
|
||||||
const tableWithSpacing = '\n' + table + '\n';
|
const tableWithSpacing = '\n' + table + '\n';
|
||||||
editor.value = before + tableWithSpacing + after;
|
tabManager.insertAtCursor(tableWithSpacing);
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + tableWithSpacing.length;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
// Trigger update
|
||||||
if (tabManager) {
|
if (tabManager) {
|
||||||
tabManager.handleEditorInput(activeTabId);
|
tabManager.handleEditorInput(tabManager.activeTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close dialog
|
// Close dialog
|
||||||
@@ -3836,30 +3764,19 @@ function insertASCIIArt() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get active editor
|
// Insert using CodeMirror
|
||||||
const activeTabId = tabManager ? tabManager.activeTabId : 1;
|
if (!tabManager) {
|
||||||
const editor = document.getElementById(`editor-${activeTabId}`);
|
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
alert('No active editor found');
|
alert('No active editor found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert in code block for proper rendering
|
// Insert in code block for proper rendering
|
||||||
const start = editor.selectionStart;
|
|
||||||
const before = editor.value.substring(0, start);
|
|
||||||
const after = editor.value.substring(editor.selectionEnd);
|
|
||||||
|
|
||||||
const codeBlock = '\n```\n' + asciiArt + '\n```\n';
|
const codeBlock = '\n```\n' + asciiArt + '\n```\n';
|
||||||
editor.value = before + codeBlock + after;
|
tabManager.insertAtCursor(codeBlock);
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
editor.selectionStart = editor.selectionEnd = start + codeBlock.length;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
// Trigger update
|
// Trigger update
|
||||||
if (tabManager) {
|
if (tabManager) {
|
||||||
tabManager.handleEditorInput(activeTabId);
|
tabManager.handleEditorInput(tabManager.activeTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close dialog
|
// Close dialog
|
||||||
@@ -4147,24 +4064,14 @@ document.addEventListener('click', (e) => {
|
|||||||
// ============================================
|
// ============================================
|
||||||
ipcRenderer.on('insert-content', (event, content) => {
|
ipcRenderer.on('insert-content', (event, content) => {
|
||||||
if (tabManager) {
|
if (tabManager) {
|
||||||
|
tabManager.insertAtCursor(content);
|
||||||
|
|
||||||
const activeTabId = tabManager.activeTabId;
|
const activeTabId = tabManager.activeTabId;
|
||||||
const editor = document.getElementById(`editor-${activeTabId}`);
|
const tab = tabManager.tabs.get(activeTabId);
|
||||||
if (editor) {
|
if (tab) {
|
||||||
const cursorPos = editor.selectionStart;
|
tab.isDirty = true;
|
||||||
const textBefore = editor.value.substring(0, cursorPos);
|
|
||||||
const textAfter = editor.value.substring(cursorPos);
|
|
||||||
|
|
||||||
editor.value = textBefore + content + textAfter;
|
|
||||||
editor.focus();
|
|
||||||
|
|
||||||
const newPos = cursorPos + content.length;
|
|
||||||
editor.setSelectionRange(newPos, newPos);
|
|
||||||
|
|
||||||
// Update state and preview
|
|
||||||
tabManager.tabs.get(activeTabId).content = editor.value;
|
|
||||||
tabManager.tabs.get(activeTabId).modified = true;
|
|
||||||
tabManager.updatePreview(activeTabId);
|
tabManager.updatePreview(activeTabId);
|
||||||
tabManager.updateTabTitle(activeTabId);
|
tabManager.updateTabBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -290,6 +290,20 @@ body {
|
|||||||
/* Color controlled by theme */
|
/* Color controlled by theme */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CodeMirror container */
|
||||||
|
.codemirror-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.codemirror-container .cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.codemirror-container .cm-scroller {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pane:last-child {
|
.pane:last-child {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* Background controlled by theme */
|
/* Background controlled by theme */
|
||||||
|
|||||||
@@ -222,6 +222,20 @@ body {
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CodeMirror container */
|
||||||
|
.codemirror-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.codemirror-container .cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.codemirror-container .cm-scroller {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pane:last-child {
|
.pane:last-child {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|||||||
Reference in New Issue
Block a user