/* * Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Virtualize only decorative editor layers. The textarea, OT document, * preview renderer, selection, history, and line link URLs are unchanged. */ export const VIRTUAL_LINE_THRESHOLD = 1500; const OVERSCAN = 8; export function buildLineStarts(text) { const starts = [0]; for (let i = 0; i < text.length; i++) { if (text.charCodeAt(i) === 10) starts.push(i + 1); } return starts; } export function visibleLineWindow(scrollTop, viewportHeight, paddingTop, lineHeight, total, overscan = OVERSCAN) { const height = Number.isFinite(viewportHeight) ? Math.max(0, viewportHeight) : 0; const top = Number.isFinite(scrollTop) ? Math.max(0, scrollTop) : 0; const rowHeight = Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 20; const offset = Number.isFinite(paddingTop) ? paddingTop : 0; const from = Math.max(0, Math.floor((top - offset) / rowHeight)); const to = Math.max(from + 1, Math.ceil((top + height - offset) / rowHeight)); return { first: Math.max(0, Math.min(total, from - overscan)), end: Math.max(0, Math.min(total, to + overscan)), }; } export function labelGroupLines(authorsByLine, ownerName) { const groups = []; let previousSignature = null; for (let i = 0; i < authorsByLine.length; i++) { const authors = authorsByLine[i] || []; if (!authors.length) continue; const signature = authors.map(ownerName).sort((a, b) => a.localeCompare(b)).join('\u0000'); if (signature === previousSignature) continue; previousSignature = signature; groups.push({ line: i, authors }); } return groups; } function lowerBound(items, value, selector = item => item) { let left = 0, right = items.length; while (left < right) { const mid = (left + right) >>> 1; if (selector(items[mid]) < value) left = mid + 1; else right = mid; } return left; } export function createVirtualEditorLayers({ editor, gutter, ownerLabels, authorshipLayer, colorFor, ownerName }) { let active = false; let starts = [0], text = '', spans = [], groups = []; let showAuthorship = false, showLabels = false, linkedLine = null; let lineHeight = 20, paddingTop = 0, paddingLeft = 0; let spacer, gutterWindow, labelCanvas, colorCanvas, colorViewport; let renderedFirst = -1, renderedEnd = -1; let lineNodes = new Map(), labelNodes = new Map(); function initialize() { if (active) return; active = true; spacer = document.createElement('div'); spacer.className = 'line-gutter-virtual-spacer'; spacer.style.position = 'relative'; spacer.style.width = '100%'; gutterWindow = document.createElement('div'); gutterWindow.className = 'line-gutter-virtual-window'; gutterWindow.style.position = 'absolute'; gutterWindow.style.left = '0'; gutterWindow.style.right = '0'; spacer.append(gutterWindow); gutter.replaceChildren(spacer); labelCanvas = document.createElement('div'); labelCanvas.className = 'owner-labels-canvas'; ownerLabels.replaceChildren(labelCanvas); colorCanvas = document.createElement('div'); colorCanvas.className = 'authorship-canvas'; colorViewport = document.createElement('div'); colorViewport.className = 'authorship-viewport'; colorViewport.style.position = 'absolute'; colorViewport.style.whiteSpace = 'pre'; colorCanvas.append(colorViewport); authorshipLayer.replaceChildren(colorCanvas); } function createLineNode() { const row = document.createElement('div'); row.className = 'line-number-row'; const button = document.createElement('button'); button.className = 'line-number-button'; button.type = 'button'; button.tabIndex = -1; row.append(button); return row; } function updateGutter(first, end, force) { if (!force && first === renderedFirst && end === renderedEnd) return; const available = [...lineNodes.values()].filter(node => { const line = Number(node.firstElementChild.dataset.line); return line < first + 1 || line > end; }); const next = new Map(), fragment = document.createDocumentFragment(); for (let i = first; i < end; i++) { const line = i + 1; const row = lineNodes.get(line) || available.pop() || createLineNode(); row.style.height = `${lineHeight}px`; const button = row.firstElementChild; if (button.dataset.line !== String(line)) { button.dataset.line = String(line); button.textContent = String(line); button.setAttribute('aria-label', `Copy link to line ${line}`); button.classList.remove('is-copied'); } button.classList.toggle('is-linked', linkedLine === line); next.set(line, row); fragment.append(row); } gutterWindow.style.top = `${first * lineHeight}px`; gutterWindow.replaceChildren(fragment); lineNodes = next; } function createLabelNode(group) { const node = document.createElement('span'); node.className = 'owner-label-group'; for (const owner of group.authors) { const badge = document.createElement('span'); badge.className = 'owner-label'; badge.style.setProperty('--owner', colorFor(owner)); badge.textContent = ownerName(owner); node.append(badge); } return node; } function updateLabels(first, end, force) { if (!showLabels) { if (labelNodes.size) labelCanvas.replaceChildren(); labelNodes.clear(); return; } if (!force && first === renderedFirst && end === renderedEnd) return; const next = new Map(), fragment = document.createDocumentFragment(); for (let i = lowerBound(groups, first, item => item.line); i < groups.length && groups[i].line < end; i++) { const group = groups[i]; const node = labelNodes.get(group.line) || createLabelNode(group); node.style.top = `${paddingTop + group.line * lineHeight + lineHeight / 2}px`; next.set(group.line, node); fragment.append(node); } labelCanvas.replaceChildren(fragment); labelNodes = next; } function updateColors(first, end, force) { if (!showAuthorship) { if (colorViewport.hasChildNodes()) colorViewport.replaceChildren(); return; } if (!force && first === renderedFirst && end === renderedEnd) return; const from = starts[first] ?? text.length; const to = end < starts.length ? starts[end] : text.length; const fragment = document.createDocumentFragment(); let cursor = from; let index = lowerBound(spans, from + 1, item => item.end); while (index < spans.length) { const span = spans[index++]; if (span.start >= to) break; const start = Math.max(cursor, span.start, from); const stop = Math.min(span.end, to); if (start > cursor) fragment.append(document.createTextNode(text.slice(cursor, start))); if (stop <= start) continue; const mark = document.createElement('span'); mark.className = 'authorship-fragment'; mark.style.setProperty('--owner', colorFor(span.owner)); mark.textContent = text.slice(start, stop); mark.title = String(span.owner).split('\u001f', 1)[0]; fragment.append(mark); cursor = stop; } if (cursor < to) fragment.append(document.createTextNode(text.slice(cursor, to))); if (end === starts.length && !text.endsWith('\n')) fragment.append(document.createTextNode('\n')); colorViewport.style.top = `${paddingTop + first * lineHeight}px`; colorViewport.style.left = `${paddingLeft}px`; colorViewport.replaceChildren(fragment); } function updateViewport(force = false) { if (!active) return; const { first, end } = visibleLineWindow( editor.scrollTop, editor.clientHeight, paddingTop, lineHeight, starts.length, ); if (!force && first === renderedFirst && end === renderedEnd) return; updateGutter(first, end, force); updateLabels(first, end, force); updateColors(first, end, force); renderedFirst = first; renderedEnd = end; } return { get active() { return active; }, update({ content, lineStarts, model, authorsByLine, full, visible, link, computedStyle }) { initialize(); text = content; starts = lineStarts; spans = model?.spans || []; showAuthorship = visible; showLabels = visible && full; linkedLine = link; const style = computedStyle; lineHeight = parseFloat(style.lineHeight) || 29; paddingTop = parseFloat(style.paddingTop) || 24; paddingLeft = parseFloat(style.paddingLeft) || 0; spacer.style.height = `${starts.length * lineHeight}px`; gutterWindow.style.lineHeight = `${lineHeight}px`; colorCanvas.style.paddingTop = style.paddingTop; colorCanvas.style.paddingRight = style.paddingRight; colorCanvas.style.paddingBottom = style.paddingBottom; colorCanvas.style.paddingLeft = style.paddingLeft; colorCanvas.style.fontFamily = style.fontFamily; colorCanvas.style.fontSize = style.fontSize; colorCanvas.style.fontWeight = style.fontWeight; colorCanvas.style.lineHeight = style.lineHeight; colorCanvas.style.letterSpacing = style.letterSpacing; colorCanvas.style.minWidth = `${editor.scrollWidth}px`; colorCanvas.style.minHeight = `${editor.scrollHeight}px`; authorshipLayer.style.left = `${editor.offsetLeft}px`; groups = showLabels ? labelGroupLines(authorsByLine, ownerName) : []; labelNodes = new Map(); updateViewport(true); }, sync() { updateViewport(); }, highlight(line) { linkedLine = line; for (const [number, row] of lineNodes) { row.firstElementChild.classList.toggle('is-linked', number === line); } }, destroy() { if (!active) return; active = false; gutter.replaceChildren(); ownerLabels.replaceChildren(); authorshipLayer.replaceChildren(); lineNodes = new Map(); labelNodes = new Map(); renderedFirst = renderedEnd = -1; groups = []; spans = []; text = ''; }, get range() { return { first: renderedFirst, end: renderedEnd }; }, }; }