K. Russell Smith
K. Russell Smith

Reputation: 183

Syntax highlighter has trouble updating text

I am trying to make a code editor using HTML:

<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=Edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel='stylesheet' href='style.css'>
</head>

<body>
    <pre id='editor'><code contenteditable='true'></code></pre>
    
    <script type='module'>
        import { highlight } from './highlighter.js';
        import { Caret } from './caret.js';
        (() =>
        {
            const editor = document.querySelector('#editor code');
            const caret = new Caret(editor);
            highlight(editor);
            editor.addEventListener('input', e =>
            {
                highlight(editor);
                e.preventDefault();
            });
            editor.addEventListener('keydown', e =>
            {
                const TAB   = 9;
                const ENTER = 13;
                switch (e.keyCode)
                {
                    // ...
                }
            });
        })();
    </script>
</body>
</html>

highlighter.js:

import { Caret } from './caret.js';
export function highlight(editor)
{
    // ...
    const NORM = '#E6E6FA';
    // ...
    const Highlighter = {
        source: editor.innerText,
        start: 0,
        curr: 0,
        // ...
        fin()
        {
            return this.curr >= this.source.length;
        },
        advance()
        {
            return this.source[this.curr++];
        },
        // ...
        scan()
        {
            let result = '';
            this.start = this.curr;
            if (this.fin())
            {
                return null;
            }
            const char = this.advance();
            let color = NORM;
            switch (char)
            {
                // ...
            }
            return {
                color,
                text: this.source.substring(this.start, this.curr),
            };
        },
    };
    let result = '';
    const caret = new Caret(editor);
    const save = caret.getPos();
    for (;;)
    {
        const lexeme = Highlighter.scan();
        if (lexeme === null)
        {
            break;
        }
        const chars = lexeme.text.split('').map(
            x => `<span style='color: ${lexeme.color};'>${x}</span>`);
        result += chars.join('');
    }
    editor.innerHTML = result;
    caret.setPos(save);
};

Basically, it takes the text content of the user's code, scans it to generate lexemes with color data, splits those lexemes into characters that are put into <span> tags with the colors, then appends those <span>s to a string that the innerHTML of the editor is updated to, and finally the user's cursor is put back to its proper position; this is done on input. There's one problem, though: if the user types too fast, their inputed text can be doubled. I've tried to remedy this with other types of event listeners, and tried simply using setInterval, but it hasn't worked well at all.

Upvotes: 1

Views: 51

Answers (1)

simmer
simmer

Reputation: 2711

From what you've described

if the user types too fast

and seeing that the computationally-expensive highlight() function is called on every change to the input element

editor.addEventListener('input', e => {
  highlight(editor); // <--
  e.preventDefault();
});

I'd suggest debouncing that call to highlight. Here's a good explainer of debouncing.

Try something like this:

// Vanilla debounce: https://gist.github.com/peduarte/7ee475dd0fae1940f857582ecbb9dc5f

function debounce(func, wait = 100) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

// ...

// adjust delay to find a balance between responsiveness and performance
const delay = 500; 

const runHighlight = () => highlight(editor);
const debouncedHighlight = debounce(runHighlight, delay);

editor.addEventListener('input', e => {
  e.preventDefault();
  debouncedHighlight();
});

Upvotes: 1

Related Questions