In the ever-evolving world of web development, interactive code editors have become an essential tool for developers, educators, and learners alike. These powerful tools allow users to write, edit, and execute code directly in the browser, providing an immersive and hands-on coding experience. In this comprehensive guide, we'll dive deep into the process of building a feature-rich JavaScript code editor using modern web technologies.

Understanding the Basics

Before we embark on our journey to create a JavaScript editor, let's first understand what makes up a typical code editor and why it's such a valuable tool.

🔍 A code editor typically consists of several key components:

  1. A text area for inputting code
  2. Syntax highlighting for improved readability
  3. Line numbers for easy reference
  4. An execution environment to run the code
  5. Output display for results and errors

By combining these elements, we can create a powerful tool that enhances the coding experience and facilitates learning and experimentation.

Setting Up the Project

Let's start by setting up the basic structure of our HTML file. We'll create a simple layout that includes areas for our code input, execution button, and output display.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JavaScript Code Editor</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="editor-container">
        <textarea id="code-input"></textarea>
        <button id="run-button">Run Code</button>
    </div>
    <div id="output-container"></div>

    <script src="editor.js"></script>
</body>
</html>

In this HTML structure, we've defined two main containers: one for the editor and another for the output. The editor container includes a textarea for code input and a button to execute the code. We've also linked a CSS file for styling and a JavaScript file where we'll implement our editor's functionality.

Styling the Editor

Now, let's add some basic styling to make our editor visually appealing and functional. Create a styles.css file and add the following CSS:

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 20px;
    background-color: #f0f0f0;
}

#editor-container {
    background-color: #fff;
    border-radius: 5px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    padding: 20px;
    margin-bottom: 20px;
}

#code-input {
    width: 100%;
    height: 300px;
    font-family: 'Courier New', Courier, monospace;
    font-size: 14px;
    resize: vertical;
    border: 1px solid #ccc;
    padding: 10px;
    box-sizing: border-box;
}

#run-button {
    background-color: #4CAF50;
    border: none;
    color: white;
    padding: 10px 20px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 16px;
    margin-top: 10px;
    cursor: pointer;
    border-radius: 3px;
}

#output-container {
    background-color: #fff;
    border-radius: 5px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    padding: 20px;
    min-height: 100px;
}

This CSS provides a clean and modern look for our editor, with a contrasting background for the code input area and a distinctive "Run Code" button.

Implementing Basic Functionality

Now that we have our structure and styling in place, let's implement the core functionality of our JavaScript editor. Create an editor.js file and add the following code:

document.addEventListener('DOMContentLoaded', () => {
    const codeInput = document.getElementById('code-input');
    const runButton = document.getElementById('run-button');
    const outputContainer = document.getElementById('output-container');

    runButton.addEventListener('click', () => {
        const code = codeInput.value;
        let output = '';

        // Capture console.log output
        const originalLog = console.log;
        console.log = (...args) => {
            output += args.join(' ') + '\n';
        };

        try {
            // Execute the code
            eval(code);
        } catch (error) {
            output += 'Error: ' + error.message;
        }

        // Restore original console.log
        console.log = originalLog;

        // Display the output
        outputContainer.textContent = output;
    });
});

Let's break down this code and understand what's happening:

  1. We wait for the DOM to be fully loaded before executing our JavaScript.
  2. We get references to our HTML elements: the code input textarea, the run button, and the output container.
  3. We add a click event listener to the run button.
  4. When the button is clicked, we capture the code from the textarea.
  5. We override the console.log function to capture its output as a string.
  6. We use eval() to execute the user's code. While eval() can be dangerous with untrusted input, it's suitable for our controlled environment.
  7. If there's an error during execution, we catch it and add it to the output.
  8. Finally, we restore the original console.log function and display the output in the output container.

🚀 With this basic implementation, users can now write JavaScript code in the textarea and see the results when they click the "Run Code" button.

Adding Syntax Highlighting

Syntax highlighting is a crucial feature of any code editor as it improves code readability and helps developers identify different parts of their code quickly. Let's implement syntax highlighting using the popular Prism.js library.

First, update your HTML file to include Prism.js and its CSS:

<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... other head elements ... -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism.min.css">
</head>
<body>
    <!-- ... your existing body content ... -->

    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-javascript.min.js"></script>
    <script src="editor.js"></script>
</body>
</html>

Now, let's modify our editor.js file to implement syntax highlighting:

document.addEventListener('DOMContentLoaded', () => {
    const codeInput = document.getElementById('code-input');
    const runButton = document.getElementById('run-button');
    const outputContainer = document.getElementById('output-container');
    const highlightedCode = document.createElement('pre');
    const codeElement = document.createElement('code');

    highlightedCode.appendChild(codeElement);
    highlightedCode.className = 'language-javascript';
    codeElement.className = 'language-javascript';

    codeInput.parentNode.insertBefore(highlightedCode, codeInput.nextSibling);
    codeInput.style.display = 'none';

    function updateHighlightedCode() {
        codeElement.textContent = codeInput.value;
        Prism.highlightElement(codeElement);
    }

    codeInput.addEventListener('input', updateHighlightedCode);

    // Initial highlighting
    updateHighlightedCode();

    runButton.addEventListener('click', () => {
        // ... (previous run button code remains the same)
    });
});

In this updated code:

  1. We create a new <pre> element to contain our highlighted code.
  2. We create a <code> element inside the <pre> element, which is where Prism.js will apply the syntax highlighting.
  3. We hide the original textarea and insert our new highlighted code elements into the DOM.
  4. We create an updateHighlightedCode function that copies the content from the textarea to our <code> element and applies Prism.js highlighting.
  5. We add an event listener to the textarea to update the highlighted code whenever the user types.
  6. We call updateHighlightedCode initially to highlight any existing code.

🎨 With these changes, our editor now features beautiful syntax highlighting, making the code much easier to read and understand.

Implementing Line Numbers

Line numbers are another essential feature of code editors, helping users navigate and reference specific parts of their code. Let's add line numbers to our editor.

First, update the CSS in your styles.css file:

/* ... previous CSS ... */

.editor-wrapper {
    position: relative;
    overflow: hidden;
}

.line-numbers {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 30px;
    background-color: #f7f7f7;
    border-right: 1px solid #ddd;
    font-family: 'Courier New', Courier, monospace;
    font-size: 14px;
    line-height: 1.5;
    padding: 10px 5px;
    text-align: right;
    color: #999;
    user-select: none;
}

.highlighted-code {
    margin-left: 40px;
    padding: 10px;
    font-family: 'Courier New', Courier, monospace;
    font-size: 14px;
    line-height: 1.5;
    overflow-x: auto;
}

Now, let's update our editor.js file to implement line numbers:

document.addEventListener('DOMContentLoaded', () => {
    const codeInput = document.getElementById('code-input');
    const runButton = document.getElementById('run-button');
    const outputContainer = document.getElementById('output-container');

    const editorWrapper = document.createElement('div');
    editorWrapper.className = 'editor-wrapper';

    const lineNumbers = document.createElement('div');
    lineNumbers.className = 'line-numbers';

    const highlightedCode = document.createElement('pre');
    highlightedCode.className = 'highlighted-code language-javascript';

    const codeElement = document.createElement('code');
    codeElement.className = 'language-javascript';

    highlightedCode.appendChild(codeElement);
    editorWrapper.appendChild(lineNumbers);
    editorWrapper.appendChild(highlightedCode);

    codeInput.parentNode.insertBefore(editorWrapper, codeInput.nextSibling);
    codeInput.style.display = 'none';

    function updateHighlightedCode() {
        codeElement.textContent = codeInput.value;
        Prism.highlightElement(codeElement);
        updateLineNumbers();
    }

    function updateLineNumbers() {
        const lines = codeInput.value.split('\n');
        lineNumbers.innerHTML = lines.map((_, index) => index + 1).join('<br>');
    }

    codeInput.addEventListener('input', updateHighlightedCode);

    // Initial highlighting and line numbers
    updateHighlightedCode();

    runButton.addEventListener('click', () => {
        // ... (previous run button code remains the same)
    });
});

In this updated code:

  1. We create a wrapper div to contain both the line numbers and the highlighted code.
  2. We create a separate div for line numbers.
  3. We update the updateHighlightedCode function to also call updateLineNumbers.
  4. We implement the updateLineNumbers function, which counts the lines in the code and updates the line numbers div accordingly.

🔢 With these changes, our editor now displays line numbers alongside the code, further enhancing its usability and professional appearance.

Adding Auto-Indent Functionality

Auto-indentation is a helpful feature that can significantly improve the coding experience. Let's implement a basic auto-indent functionality that adds appropriate indentation when the user presses Enter.

Update your editor.js file with the following code:

document.addEventListener('DOMContentLoaded', () => {
    // ... (previous code remains the same)

    function handleKeyDown(event) {
        if (event.key === 'Tab') {
            event.preventDefault();
            const start = this.selectionStart;
            const end = this.selectionEnd;
            const spaces = '    '; // 4 spaces for indentation
            this.value = this.value.substring(0, start) + spaces + this.value.substring(end);
            this.selectionStart = this.selectionEnd = start + spaces.length;
            updateHighlightedCode();
        } else if (event.key === 'Enter') {
            event.preventDefault();
            const start = this.selectionStart;
            const end = this.selectionEnd;
            const currentLine = this.value.substring(0, start).split('\n').pop();
            const indent = currentLine.match(/^\s*/)[0];
            const newLine = '\n' + indent;
            this.value = this.value.substring(0, start) + newLine + this.value.substring(end);
            this.selectionStart = this.selectionEnd = start + newLine.length;
            updateHighlightedCode();
        }
    }

    codeInput.addEventListener('keydown', handleKeyDown);

    // ... (rest of the code remains the same)
});

Let's break down the new handleKeyDown function:

  1. When the Tab key is pressed, we prevent the default behavior and insert four spaces instead.
  2. When the Enter key is pressed, we prevent the default behavior and add a new line with the same indentation as the current line.
  3. In both cases, we update the cursor position and call updateHighlightedCode to reflect the changes.

🚀 With this addition, our editor now supports basic auto-indentation, making it easier for users to write well-formatted code.

Implementing Code Completion

Code completion is an advanced feature that can greatly enhance the coding experience. While implementing a full-fledged code completion system is beyond the scope of this article, we can add a simple version that suggests common JavaScript keywords and built-in functions.

First, let's create an array of suggestions in our editor.js file:

const suggestions = [
    'console.log', 'function', 'return', 'if', 'else', 'for', 'while', 'let', 'const', 'var',
    'Array', 'Object', 'String', 'Number', 'Boolean', 'Math', 'Date', 'RegExp', 'Map', 'Set',
    'Promise', 'async', 'await', 'try', 'catch', 'throw', 'class', 'extends', 'super', 'this'
];

Now, let's implement a basic code completion function:

document.addEventListener('DOMContentLoaded', () => {
    // ... (previous code remains the same)

    const suggestionList = document.createElement('ul');
    suggestionList.className = 'suggestions';
    editorWrapper.appendChild(suggestionList);

    function showSuggestions() {
        const cursorPosition = codeInput.selectionStart;
        const textBeforeCursor = codeInput.value.substring(0, cursorPosition);
        const words = textBeforeCursor.split(/\s+/);
        const currentWord = words[words.length - 1];

        if (currentWord.length < 2) {
            suggestionList.innerHTML = '';
            return;
        }

        const matchingSuggestions = suggestions.filter(s => s.startsWith(currentWord));

        suggestionList.innerHTML = matchingSuggestions
            .map(s => `<li>${s}</li>`)
            .join('');

        const rect = codeElement.getBoundingClientRect();
        suggestionList.style.left = `${rect.left}px`;
        suggestionList.style.top = `${rect.top + rect.height}px`;
    }

    function handleSuggestionClick(event) {
        if (event.target.tagName === 'LI') {
            const suggestion = event.target.textContent;
            const cursorPosition = codeInput.selectionStart;
            const textBeforeCursor = codeInput.value.substring(0, cursorPosition);
            const words = textBeforeCursor.split(/\s+/);
            const currentWord = words[words.length - 1];

            const newValue = codeInput.value.substring(0, cursorPosition - currentWord.length) +
                             suggestion +
                             codeInput.value.substring(cursorPosition);

            codeInput.value = newValue;
            codeInput.selectionStart = codeInput.selectionEnd = cursorPosition - currentWord.length + suggestion.length;

            updateHighlightedCode();
            suggestionList.innerHTML = '';
        }
    }

    codeInput.addEventListener('input', showSuggestions);
    suggestionList.addEventListener('click', handleSuggestionClick);

    // ... (rest of the code remains the same)
});

Let's add some CSS to style our suggestion list:

.suggestions {
    position: absolute;
    background-color: #fff;
    border: 1px solid #ddd;
    max-height: 200px;
    overflow-y: auto;
    list-style-type: none;
    padding: 0;
    margin: 0;
    z-index: 1000;
}

.suggestions li {
    padding: 5px 10px;
    cursor: pointer;
}

.suggestions li:hover {
    background-color: #f0f0f0;
}

This implementation does the following:

  1. We create a suggestion list element and append it to our editor wrapper.
  2. The showSuggestions function checks the current word being typed and displays matching suggestions.
  3. The handleSuggestionClick function handles clicks on suggestions, inserting the selected suggestion into the code.
  4. We add event listeners for input (to show suggestions) and clicks on the suggestion list.

🧠 With these additions, our editor now offers basic code completion, suggesting common JavaScript keywords and functions as the user types.

Conclusion

In this comprehensive guide, we've built a feature-rich JavaScript code editor from scratch. Our editor now includes:

  • Syntax highlighting for improved code readability
  • Line numbers for easy code navigation
  • Auto-indentation for better code formatting
  • Basic code completion for common JavaScript keywords and functions

While there's always room for more advanced features (like error checking, multiple file support, or integration with language servers), this editor provides a solid foundation for coding practice and experimentation.

Remember, building a code editor is an iterative process. As you use your editor, you'll likely come up with ideas for improvements and additional features. Don't hesitate to expand upon this base and make it your own!

Happy coding! 🚀👨‍💻👩‍💻