/**
* Editor Manager - Handles editor functionality like gutter, status bar, formatting
*/
// Import syntax highlighter
const { highlightCppSyntax, applySyntaxHighlight, shouldHighlight } = require('../utils/syntaxHighlighter');
const { detectFileType } = require('../utils/fileTypeUtils');
class EditorManager {
constructor() {
this.editor = document.getElementById('editor');
this.gutter = document.getElementById('gutter');
this.lineCounter = document.getElementById('lineCounter');
this.currentFileType = 'Plain Text';
this.highlightEnabled = false;
this.updateTimer = null;
this.isUpdating = false;
this.init();
}
init() {
if (!this.editor || !this.gutter || !this.lineCounter) return;
// Maintain visual selection when editor loses focus
this.setupPersistentSelection();
// Make TAB key insert tab character in editor
this.editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
document.execCommand('insertText', false, '\t');
} else if (e.key === 'Enter') {
e.preventDefault();
document.execCommand('insertLineBreak');
}
});
// Enhanced input handler with debounced syntax highlighting
this.editor.addEventListener('input', (e) => {
if (!this.isUpdating) {
this.updateGutter();
this.updateStatusBar();
this.debouncedHighlightUpdate();
}
});
this.editor.addEventListener('scroll', () => {
this.syncScroll();
});
this.editor.addEventListener('click', () => {
this.updateStatusBar();
this.updateGutter();
});
this.editor.addEventListener('keyup', () => {
this.updateStatusBar();
this.updateGutter();
});
// Initialize
this.updateGutter();
this.updateStatusBar();
}
/**
* Save current cursor position
*/
saveCursorPosition() {
const selection = window.getSelection();
if (!selection.rangeCount) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(this.editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
/**
* Restore cursor position
*/
restoreCursorPosition(offset) {
if (offset === null || offset === undefined) return;
const selection = window.getSelection();
const range = document.createRange();
let currentOffset = 0;
const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const nodeLength = node.textContent.length;
if (currentOffset + nodeLength >= offset) {
const targetOffset = offset - currentOffset;
range.setStart(node, Math.min(targetOffset, node.textContent.length));
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
return;
}
currentOffset += nodeLength;
}
// If we didn't find the position, place cursor at end
if (this.editor.lastChild) {
range.selectNodeContents(this.editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
/**
* Update syntax highlighting with debouncing
*/
debouncedHighlightUpdate() {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
}
this.updateTimer = setTimeout(() => {
this.updateSyntaxHighlight();
}, 150);
}
/**
* Update syntax highlighting directly in editor
*/
updateSyntaxHighlight() {
if (!this.editor || this.isUpdating) return;
this.isUpdating = true;
const savedOffset = this.saveCursorPosition();
// Get plain text content
const text = this.getContent();
// Apply syntax highlighting if enabled
if (this.highlightEnabled && shouldHighlight(this.currentFileType)) {
const highlighted = applySyntaxHighlight(text, this.currentFileType);
this.editor.innerHTML = highlighted;
} else {
this.editor.textContent = text;
}
// Restore cursor position
this.restoreCursorPosition(savedOffset);
this.isUpdating = false;
}
/**
* Sync gutter scroll with editor
*/
syncScroll() {
if (this.gutter && this.editor) {
this.gutter.scrollTop = this.editor.scrollTop;
}
}
/**
* Enhanced gutter line numbers with proper formatting (like VS Code)
*/
updateGutter() {
if (!this.editor || !this.gutter) return;
const text = this.getContent();
// Count lines properly - if text ends with newline, don't count it as extra line
let lines = 1;
if (text) {
const splitLines = text.split('\n');
lines = splitLines.length;
// If last element is empty string (trailing newline), don't count it
if (splitLines[splitLines.length - 1] === '') {
lines = Math.max(1, lines - 1);
}
}
// Get current line number for highlighting
const currentLine = this.getCurrentLineNumber();
const maxDigits = Math.max(2, lines.toString().length);
// Clear gutter and rebuild with styled line numbers
this.gutter.innerHTML = '';
// Create line numbers with current line highlighted
for (let i = 1; i <= lines; i++) {
const lineSpan = document.createElement('div');
lineSpan.textContent = i.toString().padStart(maxDigits, ' ');
lineSpan.style.lineHeight = '20px';
if (i === currentLine) {
lineSpan.style.color = '#f0f6fc';
lineSpan.style.fontWeight = 'bold';
lineSpan.style.fontSize = '13px';
} else {
lineSpan.style.color = '#6e7681';
lineSpan.style.fontSize = '12px';
}
this.gutter.appendChild(lineSpan);
}
// Update gutter width based on content
const charWidth = 8; // Approximate character width in monospace font
this.gutter.style.width = Math.max(60, (maxDigits + 2) * charWidth) + 'px';
}
/**
* Get current line number where cursor is
*/
getCurrentLineNumber() {
if (!this.editor) return 1;
const selection = window.getSelection();
if (!selection.rangeCount) return 1;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(this.editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
const textBeforeCursor = preCaretRange.toString();
const lines = textBeforeCursor.split('\n');
return lines.length;
}
/**
* Enhanced status bar with cursor position tracking
*/
updateStatusBar() {
if (!this.editor || !this.lineCounter) return;
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(this.editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
const textBeforeCursor = preCaretRange.toString();
const lines = textBeforeCursor.split('\n');
const line = lines.length;
const col = lines[lines.length - 1].length + 1;
if (range.toString().length > 0) {
const selectedText = range.toString();
const selectedLines = selectedText.split('\n').length;
this.lineCounter.textContent = `Ln ${line}, Col ${col} (${selectedText.length} chars, ${selectedLines} lines selected)`;
} else {
this.lineCounter.textContent = `Ln ${line}, Col ${col}`;
}
}
/**
* Jump to specific line in editor
* @param {number} lineNumber - Line number to jump to
*/
jumpToLine(lineNumber) {
console.log('Jumping to line:', lineNumber);
if (!this.editor) {
console.error('Editor not found');
return;
}
const text = this.getContent();
const lines = text.split('\n');
console.log('Total lines in editor:', lines.length);
if (lineNumber > lines.length) {
console.warn('Line number exceeds file length');
return;
}
// Calculate character position
let position = 0;
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
position += lines[i].length + 1; // +1 for newline
}
console.log('Calculated position:', position);
// Create range and set cursor
const range = document.createRange();
const sel = window.getSelection();
let currentPos = 0;
let targetNode = null;
let targetOffset = 0;
// Find the text node and offset
const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const nodeLength = node.textContent.length;
if (currentPos + nodeLength >= position) {
targetNode = node;
targetOffset = position - currentPos;
break;
}
currentPos += nodeLength;
}
if (targetNode) {
range.setStart(targetNode, targetOffset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
// Scroll into view
const lineHeight = 20;
const targetScrollTop = Math.max(0, (lineNumber - 10) * lineHeight);
this.editor.scrollTop = targetScrollTop;
}
this.editor.focus();
this.updateStatusBar();
console.log('Successfully jumped to line:', lineNumber);
}
/**
* Enhanced formatting
*/
formatCode() {
if (!this.editor) return;
const text = this.getContent();
const lines = text.split('\n');
let indentLevel = 0;
const formatted = lines.map(line => {
const trimmed = line.trim();
if (trimmed.endsWith('{') || trimmed.endsWith(':')) {
const result = ' '.repeat(indentLevel) + trimmed;
indentLevel++;
return result;
} else if (trimmed.startsWith('}')) {
indentLevel = Math.max(0, indentLevel - 1);
return ' '.repeat(indentLevel) + trimmed;
} else {
return ' '.repeat(indentLevel) + trimmed;
}
}).join('\n');
this.setContent(formatted);
return formatted;
}
/**
* Toggle word wrap
*/
toggleWordWrap() {
if (!this.editor) return;
this.editor.style.whiteSpace = this.editor.style.whiteSpace === 'pre-wrap' ? 'pre' : 'pre-wrap';
}
/**
* Get editor content
* @returns {string} - Current editor content
*/
getContent() {
if (!this.editor) return '';
// Replace <br> tags with newlines and get text content
return this.editor.textContent || '';
}
/**
* Set editor content
* @param {string} content - Content to set
*/
setContent(content) {
if (this.editor) {
this.editor.textContent = content;
this.updateGutter();
this.updateStatusBar();
this.updateSyntaxHighlight();
}
}
/**
* Set file type (placeholder for future use)
* @param {string} filename - The filename to detect type from
*/
setFileType(filename) {
this.currentFileType = detectFileType(filename);
// Enable syntax highlighting for C/C++ files
this.highlightEnabled = shouldHighlight(this.currentFileType);
// Update highlighting immediately
this.updateSyntaxHighlight();
}
/**
* Setup persistent selection highlighting (like VS Code)
* Keeps selection visually highlighted even when editor loses focus
*/
setupPersistentSelection() {
// Create a canvas to measure text and draw highlights
const editorArea = document.getElementById('editor-area');
if (!editorArea) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '12px JetBrains Mono, monospace';
// Create highlight overlay that will contain absolute positioned divs
const highlightOverlay = document.createElement('div');
highlightOverlay.id = 'selection-highlight-overlay';
highlightOverlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
`;
// Insert overlay after gutter but before editor
const gutter = document.getElementById('gutter');
if (gutter && gutter.nextSibling) {
editorArea.insertBefore(highlightOverlay, gutter.nextSibling);
} else {
editorArea.appendChild(highlightOverlay);
}
// Make editor background transparent so overlay shows through
this.editor.style.position = 'relative';
this.editor.style.zIndex = '1';
this.editor.style.background = 'transparent';
let savedSelection = null;
const updateHighlight = () => {
highlightOverlay.innerHTML = '';
if (!savedSelection || !this.editor.value) {
return;
}
const { start, end } = savedSelection;
const text = this.editor.value;
const lines = text.split('\n');
// Get editor dimensions and position
const editorRect = this.editor.getBoundingClientRect();
const editorAreaRect = editorArea.getBoundingClientRect();
const gutterWidth = 61; // gutter width + border
const lineHeight = 20;
const paddingLeft = 20;
const paddingTop = 20;
// Calculate which lines contain the selection
let charCount = 0;
let startLine = -1, startCol = -1;
let endLine = -1, endCol = -1;
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length;
if (startLine === -1 && charCount + lineLength >= start) {
startLine = i;
startCol = start - charCount;
}
if (endLine === -1 && charCount + lineLength >= end) {
endLine = i;
endCol = end - charCount;
break;
}
charCount += lineLength + 1; // +1 for newline
}
if (startLine === -1 || endLine === -1) return;
// Draw highlight rectangles for each line in selection
for (let line = startLine; line <= endLine; line++) {
const lineText = lines[line];
let colStart = (line === startLine) ? startCol : 0;
let colEnd = (line === endLine) ? endCol : lineText.length;
// Measure text width to get exact position
const beforeText = lineText.substring(0, colStart);
const selectedText = lineText.substring(colStart, colEnd);
const startX = ctx.measureText(beforeText).width;
const width = ctx.measureText(selectedText).width;
// Create highlight div for this line
const highlight = document.createElement('div');
highlight.style.cssText = `
position: absolute;
left: ${gutterWidth + paddingLeft + startX}px;
top: ${paddingTop + (line * lineHeight) - this.editor.scrollTop}px;
width: ${width + 2}px;
height: ${lineHeight}px;
background: #3a3d41;
pointer-events: none;
`;
highlightOverlay.appendChild(highlight);
}
};
// Save selection when editor loses focus
this.editor.addEventListener('blur', () => {
const start = this.editor.selectionStart;
const end = this.editor.selectionEnd;
if (start !== end) {
savedSelection = { start, end };
updateHighlight();
}
});
// Clear highlight when editor gains focus
this.editor.addEventListener('focus', () => {
if (savedSelection) {
// Restore the selection
this.editor.setSelectionRange(savedSelection.start, savedSelection.end);
}
savedSelection = null;
highlightOverlay.innerHTML = '';
});
// Update highlight position on scroll
this.editor.addEventListener('scroll', () => {
if (savedSelection) {
updateHighlight();
}
});
// Clear on text change
this.editor.addEventListener('input', () => {
if (savedSelection) {
savedSelection = null;
highlightOverlay.innerHTML = '';
}
});
}
escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* Focus the editor
*/
focus() {
if (this.editor) {
this.editor.focus();
}
}
}
module.exports = EditorManager;