SOURCE

console 命令行工具 X clear

                    
>
console
let box = document.querySelector('.box');
let text = box.querySelector('.text');
let lines = text.innerText.split('\n');
let font_metrics = box.querySelector('.font-metrics');
let markers = box.querySelector('.marker');
let textInput = box.querySelector('.text-input');
let gutter = document.querySelector('.gutter');
let gutterInnerY = gutter.querySelector('.innerY');
let gutterLayer = gutter.querySelector('.layer');
let container = document.querySelector('.container');
let lh = 20;
let fz = 13;
let renderCount = Math.floor(box.clientHeight / lh) + 2;
let renderStart = 0;
let innerX = box.querySelector('.innerX');
let innerY = box.querySelector('.innerY');
let cursor = box.querySelector('.cursor');
let anchor = {
    row: 0,
    col: 0
}
let lead = {
    row: 0,
    col: 0
}
let lastCol = 0;
let startSelect = false;
let ranges = [];
let oldSnippets = [];
let inMultiSelectMode = false;
let willClearSelection = false;
let cursorBlinkId = null;
let offsetXInBox = 0;
let offsetYInBox = 0;
let longest = {
    line: '',
    width: 0,
    row: 0
}
let keys = {
    control: false,
    shift: false,
    alt: false
}
let brackets = {
    '{': '}',
    '[': ']',
    '(': ')',
    "'": "'",
    '"': '"'
}
function Range(startRow, startCol, endRow, endCol) {
    this.start = {
        row: startRow,
        col: startCol
    }
    this.end = {
        row: endRow,
        col: endCol
    }
}
Range.prototype.setStart = function (row, col) {
    this.start = {
        row,
        col
    }
}
Range.prototype.setEnd = function (row, col) {
    this.end = {
        row,
        col
    }
}
let fontMetrics = {
    charSizes: {},
    measure(char) {
        let charWidth = parseFloat(this.charSizes[char]);
        if (Number.isNaN(charWidth)) {
            font_metrics.textContent = char;
            this.charSizes[char] = charWidth = font_metrics.getBoundingClientRect().width;
        }
        return charWidth;
    }
}
function measureText(text) {
    if (!text) {
        return 0;
    }
    let width = 0;
    for (let s of text) {
        width += fontMetrics.measure(s);
    }
    return width;
}
function debounce(callback, time) {
    return function () {
        clearTimeout(callback.timeouId);
        callback.timeouId = setTimeout(callback, time);
    }
}
function throttle(callback, time) {
    return function () {
        if (callback.wait) {
            return;
        }
        callback();
        callback.wait = true;
        setTimeout(() => {
            callback();
            callback.wait = false;
        }, time);
    }
}
function clipChildren(el, start, end) {
    if (start > end) {
        return;
    }
    let children = [];
    for (let i = 0; i < el.childElementCount; ++i) {
        let child = el.children[i];
        if (!child) {
            continue;
        }
        if (i < start || i >= end) {
            children.push(child);
        }
    }
    children.forEach(child => {
        child.remove();
    });
    return children;
}
let recoverBlink = debounce(function () {
    startCursorBlink();
}, 600);
function startCursorBlink() {
    let isVisible = getComputedStyle(cursor).display == 'block';
    if (isVisible) {
        cursor.style.display = 'none'
    } else {
        cursor.style.display = 'block';
    }
    cursorBlinkId = setTimeout(() => {
        startCursorBlink();
    }, 600);
}
function stopCursorBlink() {
    cursor.style.display = 'block';
    clearTimeout(cursorBlinkId);
}
function stopBlink() {
    stopCursorBlink();
    recoverBlink();
}
function scrollIfNeeded() {
    let scrollLines = Math.floor(box.scrollTop / lh);
    let baseRenderCount = Math.floor(box.clientHeight / lh);
    let lineHead = lines[lead.row].slice(0, lead.col);
    let offsetX = measureText(lineHead);
    let scrollLeft = box.scrollLeft;
    if (lead.row < scrollLines) {
        box.scroll(box.scrollLeft, lead.row * lh);
    } else if (lead.row > scrollLines + baseRenderCount) {
        box.scroll(box.scrollLeft, (lead.row - (scrollLines + baseRenderCount)) * lh + box.scrollTop);
    }
    if (offsetX < scrollLeft) {
        box.scroll(offsetX, box.scrollTop);
    } else if (offsetX > box.scrollLeft + box.clientWidth) {
        let sl = (offsetX - (box.scrollLeft + box.clientWidth)) + box.scrollLeft + 2;
        box.scroll(sl, box.scrollTop);
    }
}

function onCursorActive() {
    stopBlink();
    scrollIfNeeded();
}
function moveCursorTo(row, col) {
    if (!isCollapsed()) {
        clearSelection();
    }
    lastCol = col;
    let pos = clipPosToDocument({ row, col });
    anchor.row = lead.row = pos.row;
    anchor.col = lead.col = pos.col;
    updateCursorPos();
    onCursorActive();
}
function moveCursorLeft() {
    if (!isCollapsed()) {
        if (isBackwards(anchor, lead)) {
            moveCursorTo(lead.row, lead.col);
        } else {
            moveCursorTo(anchor.row, anchor.col);
        }
        return;
    }
    if (lead.col <= 0) {
        let prevLine = lines[lead.row - 1];
        let col = prevLine ? prevLine.length : 0;
        moveCursorTo(lead.row - 1, col);
    } else {
        moveCursorTo(lead.row, lead.col - 1);
    }
}
function moveCursorRight() {
    if (!isCollapsed()) {
        if (isBackwards(anchor, lead)) {
            moveCursorTo(anchor.row, anchor.col);
        } else {
            moveCursorTo(lead.row, lead.col);
        }
        return;
    }
    let line = lines[lead.row];
    if (lead.col >= line.length) {
        moveCursorTo(lead.row + 1, 0);
    } else {
        moveCursorTo(lead.row, lead.col + 1);
    }
}
function moveCursorDown() {
    moveCursorTo(lead.row + 1, lastCol);
}
function moveCursorUp() {
    moveCursorTo(lead.row - 1, lastCol);
}
function moveCursorLineStart() {
    moveCursorTo(lead.row, 0);
}
function moveCursorLineEnd() {
    moveCursorTo(lead.row, lines[lead.row].length);
}
function selectTo(row, col) {
    lastCol = col;
    let pos = clipPosToDocument({ row, col });
    lead.row = pos.row;
    lead.col = pos.col;
    updateSelection();
    updateCursorPos();
    onCursorActive();
}
function selectLeft() {
    if (lead.col == 0) {
        let prevLine = lines[lead.row - 1];
        let col = prevLine ? prevLine.length : 0;
        selectTo(lead.row - 1, col);
    } else {
        selectTo(lead.row, lead.col - 1);
    }
}
function selectRight() {
    let line = lines[lead.row];
    if (lead.col == line.length) {
        selectTo(lead.row + 1, 0);
    } else {
        selectTo(lead.row, lead.col + 1);
    }
}
function select(start, end) {
    let _anchor = clipPosToDocument({ row: start.row, col: start.col });
    let _lead = clipPosToDocument({ row: end.row, col: end.col });
    anchor.row = _anchor.row;
    anchor.col = _anchor.col;
    selectTo(_lead.row, _lead.col);
}
function selectAll() {
    let maxLineNo = lines.length - 1;
    let lastLine = lines[maxLineNo];
    select({ row: 0, col: 0 }, { row: maxLineNo, col: lastLine.length });
}
function selectRow(row) {
    let pos = clipPosToDocument({ row, col: 0 });
    let line = lines[pos.row];
    select({ row: pos.row, col: 0 }, { row: pos.row, col: line.length });
}
function insertText(text) {
    if (!isCollapsed()) {
        backspace();
    }
    _insertText(text);
    if (text in brackets) {
        moveCursorRight();
    } else {
        moveCursorTo(lead.row, lead.col + text.length);
    }
    renderVisibleLines();

    if (lines[longest.row] != longest.line) {
        updateScrollWidth();
    }
}
function _insertText(text) {
    let line = lines[lead.row];
    lines[lead.row] = line.slice(0, lead.col) + text + (text in brackets ? brackets[text] : '') + line.slice(lead.col);
}
function insertLines(inputLines) {
    if (!isCollapsed()) {
        backspace();
    }
    inputLines.forEach((line, index) => {
        let curRow = lead.row;
        let curLine = lines[curRow];
        let curLineHead = curLine.slice(0, lead.col);
        let curLineTail = curLine.slice(lead.col);
        lines[curRow] = curLineHead + line + curLineTail;
        anchor.col = lead.col = lead.col + line.length;
        if (index < inputLines.length - 1) {
            curLine = lines[lead.row];
            lines[curRow] = curLine.slice(0, lead.col);
            lines.splice(curRow + 1, 0, curLine.slice(lead.col));
            let pos = clipPosToDocument({ row: curRow + 1, col: 0 });
            anchor.col = lead.col = pos.col;
            anchor.row = lead.row = pos.row;
        }
    });

    updateLineNumber();
    updateScrollHeight();
    updateScrollWidth();
    renderVisibleLines();
    moveCursorTo(lead.row, lead.col);
}
function backspace() {
    let rows = Math.abs(anchor.row - lead.row);
    if (isCollapsed()) {
        let line = lines[lead.row];
        let row = lead.row;
        if (lead.col == 0 && lead.row > 0) {
            moveCursorLeft();
            let deleteLine = lines.splice(row, 1);
            lines[lead.row] += deleteLine;
            renderVisibleLines();
            rows = 1;
        } else if (lead.col > 0) {
            lines[lead.row] = line.slice(0, lead.col - 1) + line.slice(lead.col);
            moveCursorLeft();
            renderVisibleLines();
        }
    } else {
        let backwards = isBackwards(anchor, lead);
        let isSame = lead.row == anchor.row;
        let startRow = backwards ? lead.row : anchor.row;
        let startCol = backwards ? lead.col : anchor.col;
        let endRow = backwards ? anchor.row : lead.row;
        let endCol = backwards ? anchor.col : lead.col;
        if (isSame) {
            let line = lines[lead.row];
            lines[lead.row] = line.slice(0, startCol) + line.slice(endCol);
            moveCursorLeft();
            renderVisibleLines();
            clearSelection();
        } else {
            let startLine = lines[startRow];
            let endLine = lines[endRow];
            let deleteCount = endRow - startRow;
            lines.splice(startRow + 1, deleteCount);
            lines[startRow] = startLine.slice(0, startCol) + endLine.slice(endCol);
            moveCursorLeft();
            renderVisibleLines();
            clearSelection();
        }
    }
    updateScrollHeight();


    box.scrollBy(0, -rows * lh);

    if (rows) {
        updateLineNumber();
    }

    if (lines[longest.row] != longest.line) {
        updateScrollWidth();
    }
}
function breakLine() {
    let row = lead.row;
    let line = lines[row];
    if (typeof line == 'string') {
        if (!isCollapsed()) {
            backspace();
            breakLine();
        } else {
            lines[row] = line.slice(0, lead.col);
            lines.splice(row + 1, 0, line.slice(lead.col));
            moveCursorTo(row + 1, 0);
            renderVisibleLines();
        }
    }

    updateScrollHeight();
    updateLineNumber();
    // if (lines[longest.row] != longest.line) {
    //     updateScrollWidth();
    // }
}
function _breakLine() {
    let row = lead.row;
    let line = lines[row];
    lines[row] = line.slice(0, lead.col);
    lines.splice(row + 1, 0, line.slice(lead.col));
}
function onPaste(e) {
    e.preventDefault();
    if (e instanceof ClipboardEvent) {
        let text = e.clipboardData.getData('text/plain');
        let _lines = text.split('\n');
        if (_lines.length == 1) {
            insertText(_lines[0]);
        } else {
            insertLines(_lines);
        }
    }
}
function cutSelection() {
    if (isCollapsed()) {
        return;
    }
    copySelectionText();
    backspace();
}
function copySelectionText() {
    navigator.clipboard.writeText(getSelectionText());
}
function renderVisibleLines() {
    let start = renderStart;
    let end = start + renderCount;
    let snippets = lines.slice(start, end);
    let code = snippets.join('\n');
    text.innerText = code;
    text.style.top = start * lh + 'px';

    oldSnippets = snippets;
}
function layout() {
    let scrollLines = Math.floor(box.scrollTop / lh);
    let maxLineNo = (renderCount + scrollLines).toString();
    let width = measureText(maxLineNo);
    let gutterPadding = 20;
    gutter.style.width = width + gutterPadding + 'px';
    box.style.left = width + gutterPadding + 'px';
    box.style.width = `calc(100% - ${width + gutterPadding}px)`;
}
function updateLineNumber() {
    let start = Math.floor(box.scrollTop / lh);
    let end = Math.min(lines.length - start, renderCount);
    let fragment = document.createDocumentFragment();
    for (let i = 0; i < end; ++i) {
        let elt = gutterLayer.children[i] || document.createElement('div');
        elt.innerHTML = start + i + 1;
        elt.classList.add('item');
        elt.style.top = (i + start) * lh + 'px';
        if (!gutterLayer.contains(elt)) {
            fragment.appendChild(elt);
        }
    }
    if (fragment.childElementCount) {
        gutterLayer.appendChild(fragment);
    }
    clipChildren(gutterLayer, 0, end);

    layout();
}
function updateScrollHeight() {
    innerY.style.height = lines.length * lh + 'px';
    gutterInnerY.style.height = lines.length * lh + 'px';
}
function updateScrollWidth() {
    longest.width = 0;
    longest.line = '';
    longest.row = 0;
    lines.forEach((line, row) => {
        let width = measureText(line);
        if (width >= longest.width) {
            longest.width = width;
            longest.line = line;
            longest.row = row;
        }
    });
    innerX.style.width = longest.width + 'px';
}
function updateCursorPos() {
    let lineHead = lines[lead.row].slice(0, lead.col);
    cursor.style.top = textInput.style.top = lead.row * lh + 'px';
    cursor.style.left = textInput.style.left = measureText(lineHead) + 'px';
}
function showCursor() {
    cursor.style.display = 'block';
}
function hideCursor() {
    cursor.style.display = 'none';
}
function drawRangeMarker(range) {
    let backwards = isBackwards(range.start, range.end);
    let startRow = backwards ? range.end.row : range.start.row;
    let endRow = backwards ? range.start.row : range.end.row;
    let startCol = backwards ? range.end.col : range.start.col;
    let endCol = backwards ? range.start.col : range.end.col;
    let start = Math.max(startRow, Math.floor(box.scrollTop / lh));
    let end = Math.min(endRow, start + renderCount);
    let fragment = document.createDocumentFragment();
    let linesPos = [];
    for (let i = start; i <= end; ++i) {
        let line = lines[i];
        let isStart = i == startRow;
        let isEnd = i == endRow;
        let isSame = startRow == endRow;
        let lineSnipptes = '';
        let lineHead = '';
        if (isSame) {
            lineSnipptes = line.slice(startCol, endCol);
            lineHead = line.slice(0, startCol);
        } else if (isStart) {
            lineSnipptes = line.slice(startCol, line.length);
            lineHead = line.slice(0, startCol);
        } else if (isEnd) {
            lineSnipptes = line.slice(0, endCol);
            lineHead = '';
        } else {
            lineSnipptes = line;
            lineHead = '';
        }

        let width = measureText(lineSnipptes);
        let left = measureText(lineHead);
        let top = i * lh;
        let elt = markers.children[i - start] || document.createElement('div');
        let eof = isEnd ? false : true;
        if (eof) {
            width += 8;
        }

        linesPos.push({
            start: left,
            end: left + width,
            elt,
            line
        });

        elt.classList.add('marker-item');
        elt.setAttribute('row', i);
        elt.style.width = width + 'px';
        elt.style.height = lh + 'px';
        elt.style.left = left + 'px';
        elt.style.top = top + 'px';
        if (!markers.contains(elt)) {
            fragment.appendChild(elt);
        }
    }

    linesPos.forEach((item, index) => {
        let prev = linesPos[index - 1];
        let next = linesPos[index + 1];
        let es = item.elt.style;

        if ((!prev && item.start > 0) || (prev && prev.end < item.start)) {
            es.borderTopLeftRadius = '3px';
        } else {
            es.borderTopLeftRadius = '';
        }
        if (!prev || prev.end < item.end || prev.start > item.end) {
            es.borderTopRightRadius = '3px';
        } else {
            es.borderTopRightRadius = '';
        }

        if ((!next && item.start > 0) || (next && next.end < item.start)) {
            es.borderBottomLeftRadius = '3px';
        } else {
            es.borderBottomLeftRadius = '';
        }
        if (!next || next.end < item.end || next.start > item.end) {
            es.borderBottomRightRadius = '3px';
        } else {
            es.borderBottomRightRadius = '';
        }

        let rl_bg, rl, rr_bg, rr;
        let children = item.elt.children;
        if (children[0]) {
            rl_bg = children[0];
        } else {
            item.elt.appendChild((rl_bg = document.createElement('div')));
        }
        rl_bg.classList.add('rl', 'bg', 'radius');

        if (children[1]) {
            rl = children[1];
        } else {
            item.elt.appendChild((rl = document.createElement('div')));
        }
        rl.classList.add('rl', 'radius');

        if (children[2]) {
            rr_bg = children[2];
        } else {
            item.elt.appendChild((rr_bg = document.createElement('div')));
        }
        rr_bg.classList.add('rr', 'bg', 'radius');

        if (children[3]) {
            rr = children[3];
        } else {
            item.elt.appendChild((rr = document.createElement('div')));
        }
        rr.classList.add('rr', 'radius');

        if (prev && prev.start < item.end && prev.end > item.end) {
            rr.style.borderTopLeftRadius = '3px';
        } else {
            rr.style.borderTopLeftRadius = '';
        }
        if (next && next.start < item.end && next.end > item.end) {
            rr.style.borderBottomLeftRadius = '3px';
        } else {
            rr.style.borderBottomLeftRadius = '';
        }

        if (next && next.start < item.start && next.end > item.start) {
            rl.style.borderBottomRightRadius = '3px';
        } else {
            rl.style.borderBottomRightRadius = '';
        }
    });

    if (fragment.childElementCount) {
        markers.appendChild(fragment);
    }

    clipChildren(markers, 0, end - start + 1);
}

function updateSelectionMarker() {
    for (let i = 0; i < ranges.length; ++i) {
        let range = ranges[i];
        drawRangeMarker(range);
    }
}
function updateSelection() {
    if (inMultiSelectMode) {

    } else {
        let first = ranges[0];
        if (!first) {
            first = new Range(anchor.row, anchor.col, lead.row, lead.col);
            ranges.push(first);
        } else {
            first.setStart(anchor.row, anchor.col);
            first.setEnd(lead.row, lead.col);
        }
    }
    updateSelectionMarker();
}
function clearSelection() {
    ranges = [];
    markers.innerHTML = "";
    willClearSelection = false;
    anchor.row = lead.row;
    anchor.col = lead.col;
}
function getRangeText(range) {
    let backwards = isBackwards(range.start, range.end);
    let startRow = backwards ? range.end.row : range.start.row;
    let endRow = backwards ? range.start.row : range.end.row;
    let startCol = backwards ? range.end.col : range.start.col;
    let endCol = backwards ? range.start.col : range.end.col;
    let _lines = [];
    for (let i = startRow; i <= endRow; ++i) {
        let isStart = i == startRow;
        let isEnd = i == endRow;
        let isSame = startRow == endRow;
        let line = lines[i];
        if (isSame) {
            _lines.push(line.slice(startCol, endCol));
        } else if (isStart) {
            _lines.push(line.slice(startCol));
        } else if (isEnd) {
            _lines.push(line.slice(0, endCol));
        } else {
            _lines.push(line);
        }
    }
    return _lines.join('\n');
}
function getSelectionText() {
    let texts = [];
    ranges.forEach(range => {
        texts.push(getRangeText(range));
    });
    return texts.join('\n');
}
function isBackwards(anchor, lead) {
    return lead.row < anchor.row || (lead.row == anchor.row && lead.col < anchor.col);
}
function isPointInRange(x, y, range) {
    let backwards = isBackwards(range.start, range.end);
    let startRow = backwards ? range.end.row : range.start.row;
    let endRow = backwards ? range.start.row : range.end.row;
    let startCol = backwards ? range.end.col : range.start.col;
    let endCol = backwards ? range.start.col : range.end.col;
    let row = findPosFromPoints(x, y).row;
    if (row < startRow || row > endRow) {
        return false;
    }
    let line = lines[row];
    if (startRow == endRow) {
        let lineHead = line.slice(0, startCol);
        let lineSnipptes = line.slice(0, endCol);
        let min = measureText(lineHead);
        let max = measureText(lineSnipptes);
        if (x < min || x > max) {
            return false;
        }
    } else if (row == startRow) {
        let lineHead = line.slice(0, startCol);
        let lineWidth = measureText(line);
        let markerOffset = measureText(lineHead);
        if (x < markerOffset || x > lineWidth) {
            return false;
        }
    } else if (row == endRow) {
        let lineHead = line.slice(0, endCol);
        let markerWidth = measureText(lineHead);
        if (x < 0 || x > markerWidth) {
            return false;
        }
    } else if (x < 0 || x > measureText(line)) {
        return false;
    }
    return true;
}
function findPosFromPoints(x, y) {
    let st = box.scrollTop;
    let sl = box.scrollLeft;
    let sx = sl + x;
    let sy = st + y;
    let startCol = 0;
    let startRow = Math.min(lines.length - 1, Math.max(Math.floor(sy / lh), 0));
    let line = lines[startRow];
    for (startCol = 0; startCol < line.length; ++startCol) {
        let head = line.slice(0, startCol);
        let lastCh = head[head.length - 1] || '';
        let charEnd = measureText(head);
        let charStart = measureText(head) - measureText(lastCh);
        if (sx >= charStart && sx <= charEnd) {
            let sub = charEnd - charStart;
            let sub2 = sx - charStart;
            if (!Math.round(sub2 / sub)) {
                --startCol;
            }
            break;
        }
    }
    return {
        col: startCol,
        row: startRow
    }
}
function clipPosToDocument(pos) {
    let p = {}
    if (pos.row > lines.length - 1) {
        p.row = lines.length - 1;
        p.col = lines[p.row].length;
    } else if (pos.row < 0) {
        p.row = 0;
        p.col = 0;
    } else {
        p.row = pos.row;
        p.col = Math.min(lines[pos.row].length, Math.max(0, pos.col));
    }
    return p;
}
function clipPosToContainer(clientX, clientY) {
    let rect = box.getBoundingClientRect();
    let x = Math.min(rect.left + box.clientWidth, Math.max(0, clientX - rect.left));
    let y = Math.min(rect.top + box.clientHeight, Math.max(0, clientY - rect.top));
    return { x, y };
}
function isCollapsed() {
    return anchor.row == lead.row && anchor.col == lead.col;
}
function isPointInWorkspace(clientX, clientY) {
    let rect = box.getBoundingClientRect();
    if (clientX < rect.left || clientX > (box.clientWidth + rect.left)) {
        return false;
    }
    if (clientY < rect.top || clientY > (box.clientHeight + rect.top)) {
        return false;
    }
    return true;
}
function startScrollDown(speed) {
    if ((typeof speed == "number" && speed < 1) || !speed) {
        speed = 1;
    } else if (speed > 3) {
        speed = 3;
    }

    startScrollDown.speed = speed;


    if (!startScrollDown.timerId) {
        stopScrollDown();

        if (Math.ceil(box.scrollTop + box.clientHeight) > box.scrollHeight) {
            return;
        }

        startScrollDown.timerId = setInterval(() => {
            let remainder = box.scrollTop % lh;
            if (remainder) {
                box.scroll(box.scrollLeft, box.scrollTop - remainder + lh);
            } else {
                box.scrollBy(0, startScrollDown.speed * lh);
            }
            if (Math.ceil(box.scrollTop + box.clientHeight) > box.scrollHeight) {
                stopScrollDown();
            }
        }, 20);
    }
}
function stopScrollDown() {
    clearInterval(startScrollDown.timerId);
    startScrollDown.timerId = null;
}


function startScrollUp(speed) {
    if ((typeof speed == "number" && speed < 1) || !speed) {
        speed = 1;
    } else if (speed > 3) {
        speed = 3;
    }
    startScrollUp.speed = speed;

    if (!startScrollUp.timerId) {
        stopScrollUp();

        if (box.scrollTop == 0) {
            return;
        }

        startScrollUp.timerId = setInterval(() => {
            let remainder = box.scrollTop % lh;
            if (remainder) {
                box.scroll(box.scrollLeft, box.scrollTop - remainder);
            } else {
                box.scrollBy(0, -startScrollUp.speed * lh);
            }
            if (box.scrollTop == 0) {
                stopScrollUp();
            }
        }, 20);
    }
}
function stopScrollUp() {
    clearInterval(startScrollUp.timerId);
    startScrollUp.timerId = null;
}

function startScrollRight() {
    if (!startScrollRight.timerId) {
        stopScrollRight();

        let lineWidth = measureText(lines[lead.row]);

        if (box.scrollLeft + box.clientWidth >= lineWidth) {
            return;
        }

        startScrollRight.timerId = setInterval(() => {
            box.scrollBy(20, 0);

            lineWidth = measureText(lines[lead.row]);

            if (box.scrollLeft + box.clientWidth >= lineWidth + 20) {
                stopScrollRight();
            }
        }, 20);
    }
}
function stopScrollRight() {
    clearInterval(startScrollRight.timerId);
    startScrollRight.timerId = null;
}


function startScrollLeft() {
    if (!startScrollLeft.timerId) {
        stopScrollLeft();

        if (box.scrollLeft == 0) {
            return;
        }

        startScrollLeft.timerId = setInterval(() => {
            box.scrollBy(-20, 0);
            if (box.scrollLeft == 0) {
                stopScrollLeft();
            }
        }, 20);
    }
}
function stopScrollLeft() {
    clearInterval(startScrollLeft.timerId);
    startScrollLeft.timerId = null;
}

function stopAutoScroll() {
    stopScrollDown();
    stopScrollUp();
    stopScrollLeft();
    stopScrollRight();
}
container.onmousedown = function () {
    setTimeout(() => {
        textInput.focus({ preventScroll: true });
    }, 0);
}
box.onmousedown = function (e) {
    if (!isPointInWorkspace(e.clientX, e.clientY)) {
        return;
    }
    let posClipped = clipPosToContainer(e.clientX, e.clientY);
    let inRange = isPointInRange(posClipped.x, posClipped.y, { start: anchor, end: lead });
    let pos = findPosFromPoints(posClipped.x, posClipped.y);
    if (inRange && !isCollapsed()) {
        willClearSelection = true;
        startSelect = false;
        box.draggable = true;
    } else {
        startSelect = true;
        box.draggable = false;
        clearSelection();
        moveCursorTo(pos.row, pos.col);
    }
}
box.onscroll = function () {
    let start = Math.floor(box.scrollTop / lh);
    if (start != renderStart) {
        renderStart = start;
        renderVisibleLines();
    }
    if (startSelect) {
        let pos = findPosFromPoints(offsetXInBox, offsetYInBox);
        selectTo(pos.row, pos.col);
    } else {
        updateSelection();
        updateCursorPos();
    }
    updateLineNumber();
    gutter.scroll(0, box.scrollTop);
}
box.ondragover = function (e) {
    e.preventDefault();
}
box.ondragstart = function (e) {
    if (e instanceof DragEvent) {
        e.dataTransfer.setDragImage(new Image(), 0, 0);
        e.dataTransfer.setData('text/plain', getSelectionText());
    }
}
box.oncontextmenu = function (e) {
    e.preventDefault();
}
box.onwheel = function (e) {
    e.preventDefault();
    if (e.deltaY < 0) {
        box.scrollBy(0, -70);
    } else {
        box.scrollBy(0, 70);
    }
}
textInput.onkeydown = function (e) {
    let k = e.key.toLowerCase();
    if (k in keys) {
        keys[k] = true;
    }
    if (keys.shift && k == 'arrowleft') {
        selectLeft();
    } else if (keys.shift && k == 'arrowright') {
        selectRight();
    } else if (keys.control && k == 'a') {
        selectAll();
    } else if (keys.control && k == 'c') {
        copySelectionText();
    } else if (keys.control && k == 'x') {
        cutSelection();
    } else if (k == 'arrowleft') {
        moveCursorLeft();
    } else if (k == 'arrowright') {
        moveCursorRight();
    } else if (k == 'arrowdown') {
        moveCursorDown();
    } else if (k == 'arrowup') {
        moveCursorUp();
    } else if (k == 'end') {
        moveCursorLineEnd();
    } else if (k == 'home') {
        moveCursorLineStart();
    } else if (k == 'backspace') {
        backspace();
    } else if (k == 'tab') {
        e.preventDefault();
        insertText(' '.repeat(4));
    }
}
textInput.oninput = function (e) {
    let it = e.inputType;
    if (it == 'insertText') {
        insertText(e.data);
    } else if (it == 'insertLineBreak') {
        breakLine();
    }
    textInput.value = '';
}
textInput.onpaste = onPaste;
gutter.onmousedown = function (e) {
    let row = Math.floor((e.clientY - gutter.getBoundingClientRect().top) / lh + parseInt(box.scrollTop / lh));
    let pos1 = clipPosToDocument({ row, col: 0 });
    let pos2 = clipPosToDocument({ row: row + 1, col: 0 });
    select({ row: pos1.row, col: pos1.col }, { row: pos2.row, col: pos2.col });
}
document.onkeyup = function (e) {
    let k = e.key.toLowerCase();
    if (k in keys) {
        keys[k] = false;
    }
}
document.onmouseup = function (e) {
    if (willClearSelection) {
        clearSelection();
        let posClipped = clipPosToContainer(e.clientX, e.clientY);
        let pos = findPosFromPoints(posClipped.x, posClipped.y);
        moveCursorTo(pos.row, pos.col);
    }
    box.draggable = false;
    startSelect = false;
    stopAutoScroll();
}
document.onmousemove = function (e) {
    if (startSelect) {
        let posClipped = clipPosToContainer(e.clientX, e.clientY);
        let pos = findPosFromPoints(posClipped.x, posClipped.y);
        let rect = box.getBoundingClientRect();
        let maxLeft = rect.left + box.clientWidth;
        let maxTop = rect.top + box.clientHeight;

        offsetXInBox = posClipped.x;
        offsetYInBox = posClipped.y;

        selectTo(pos.row, pos.col);

        if (e.clientX >= maxLeft) {
            startScrollRight();
        } else {
            stopScrollRight();
        }

        if (e.clientX <= rect.left) {
            startScrollLeft();
        } else {
            stopScrollLeft();
        }

        if (e.clientY >= maxTop) {
            let speed = Math.floor((e.clientY - maxTop) / 10);
            startScrollDown(speed);
        } else {
            stopScrollDown();
        }

        if (e.clientY <= rect.top) {
            let speed = Math.floor((rect.top - e.clientY) / 10);
            startScrollUp(speed);
        } else {
            stopScrollUp();
        }
    }
}
window.onresize = function () {
    renderCount = Math.floor(box.clientHeight / lh) + 2;
    renderVisibleLines();
    updateSelection();
}
renderVisibleLines();
updateScrollHeight();
updateScrollWidth();
startCursorBlink();
updateLineNumber();
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=, initial-scale=">
    <meta http-equiv="X-UA-Compatible" content="">
    <title></title>
</head>

<body>
    <div class="container">
        <div class="gutter">
            <div class="innerY"></div>
            <div class="layer"></div>
        </div>
        <div class="box">
            <div class="marker"></div>
            <pre class="text"></pre>
            <pre class="font-metrics"></pre>
            <div class="scrollbar">
                <div class="innerX"></div>
                <div class="innerY"></div>
            </div>
            <div class="cursor"></div>
            <textarea spellcheck="false" autofocus="false" class="text-input"></textarea>
        </div>
    </div>
</body>

</html>
* {
    margin: 0;
    padding: 0;
}
.container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: #23272e;
    user-select: none;
    overflow: hidden;
}
.gutter {
    position: absolute;
    left: 0;
    top: 0;
    overflow: hidden;
    height: 100%;
    font-size: 13px;
    font-family: Consolas;
    border-right: 1px solid #3b4048;
    box-sizing: border-box;
}
.gutter .item {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 20px;
    color: #abb2bf;
    text-indent: 10px;
    line-height: 20px;
}
.gutter .innerY {
    position: absolute;
    left: 0;
    top: 0;
    width: 1px;
    opacity: 0;
    z-index: -1;
}
.box {
    position: absolute;
    width: 100%;
    height: 100%;
    overflow: auto;
}
.box .text {
    position: absolute;
    left: 0;
    top: 0;
    font-size: 13px;
    font-family: Consolas;
    line-height: 20px;
    color: #abb2bf;
    width: 100%;
    z-index: 1;
    cursor: text;
}
.box .font-metrics {
    position: absolute;
    left: 0;
    top: 0;
    font-size: 13px;
    font-family: Consolas;
    margin: 0;
    padding: 0;
    z-index: -1;
    opacity: 0;
    border: none;
}
.box .innerY {
    position: absolute;
    left: 0;
    top: 0;
    width: 1px;
    z-index: -1;
    opacity: 0;
}
.box .innerX {
    position: absolute;
    left: 0;
    top: 0;
    z-index: -1;
    opacity: 0;
    height: 1px;
}
.box .cursor {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 2;
    width: 0;
    height: 20px;
    font-size: 0;
    border-left: 2px solid #24acf2;
    cursor: text;
}
.box .marker {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
}
.box .marker-item {
    position: absolute;
    background: rgba(255, 255, 255, 0.2);
}
.box .marker-item .radius {
    position: absolute;
    top: 0;
    width: 7px;
    height: 20px;
    background: #23272e;
}
.box .marker-item .bg {
    background: rgba(255, 255, 255, 0.2);
    width: 6px;
}
.box .marker-item .rl {
    right: 100%;
}
.box .marker-item .rr {
    left: 100%;
}
.box .text-input {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 2;
    outline: none;
    margin: 0;
    padding: 0;
    width: 1px;
    height: 1px;
    font-size: 1px;
    resize: none;
    border: none;
    overflow: hidden;
    opacity: 0;
}
.box::-webkit-scrollbar {
    width: 12px;
    height: 12px;
}
.box::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.1);
}
.box::-webkit-scrollbar-corner {
    background: transparent;
}
.box::-webkit-scrollbar-thumb:hover {
    background: rgba(255, 255, 255, 0.3);
}