TypeScript Tutorial: Building a Simple Web-Based Code Editor with Code Folding

In the world of web development, we often encounter the need to manage and navigate large codebases. One crucial feature that significantly enhances code readability and maintainability is code folding. Code folding allows developers to collapse and expand sections of code, such as functions, loops, or conditional statements, enabling them to focus on specific parts of the code and reduce visual clutter. In this tutorial, we’ll dive into building a simple, yet functional, web-based code editor with code folding capabilities using TypeScript. This project is ideal for beginners and intermediate developers looking to expand their skills in web development and TypeScript.

Understanding Code Folding

Before we start building, let’s understand the core concept of code folding. Code folding, also known as code collapsing, is a feature that allows a developer to hide or display blocks of code. These blocks are typically defined by structural elements like:

  • Functions
  • Loops (for, while)
  • Conditional statements (if, else)
  • Code blocks within curly braces

When a code block is folded, only its header (e.g., function signature, loop declaration) is visible, along with a visual indicator (usually a plus or minus sign) to show whether the block is collapsed or expanded. Clicking the indicator toggles the visibility of the code block. This feature is particularly useful for:

  • Navigating Large Codebases: Quickly jump between different parts of a file.
  • Reducing Clutter: Focus on the code that is currently relevant.
  • Improving Readability: Make complex code easier to understand by hiding unnecessary details.

Setting Up the Project

To get started, we’ll need to set up a basic TypeScript project. We’ll use HTML, CSS, and JavaScript for the front-end, and TypeScript for our code editor logic. Here’s a step-by-step guide:

1. Project Initialization

Create a new project directory and navigate into it using your terminal:

mkdir code-folding-editor
cd code-folding-editor

2. Initialize npm

Initialize a new npm project:

npm init -y

3. Install TypeScript

Install TypeScript as a development dependency:

npm install --save-dev typescript

4. Create tsconfig.json

Create a tsconfig.json file in the project root. This file configures the TypeScript compiler. A basic configuration is as follows:

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

5. Create Project Structure

Create the following file structure:

code-folding-editor/
│
├── src/
│   ├── index.ts
│   └── style.css
├── dist/
├── index.html
├── tsconfig.json
└── package.json

6. Create index.html

Create an index.html file with the basic HTML structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Code Folding Editor</title>
    <link rel="stylesheet" href="src/style.css">
</head>
<body>
    <div id="editor-container">
        <textarea id="code-editor"></textarea>
    </div>
    <script src="dist/index.js"></script>
</body>
</html>

7. Create style.css

Create a basic style.css file for styling the editor:

#editor-container {
    width: 80%;
    margin: 20px auto;
}

#code-editor {
    width: 100%;
    height: 500px;
    font-family: monospace;
    font-size: 14px;
    padding: 10px;
    box-sizing: border-box;
}

Implementing Code Folding in TypeScript

Now, let’s dive into the core of our project: implementing code folding in TypeScript. We’ll focus on the following steps:

  • Detecting foldable code blocks
  • Rendering folding indicators
  • Handling click events to expand and collapse blocks

1. Detecting Foldable Blocks

We need a mechanism to identify the start and end of foldable blocks. For simplicity, we’ll focus on functions and use a regular expression to find them. This is a simplified approach, and more complex editors might use a full parser for accurate folding.

In src/index.ts, add the following code:

// src/index.ts
const codeEditor = document.getElementById('code-editor') as HTMLTextAreaElement;

function findFoldableBlocks(code: string): { start: number; end: number; }[] {
    const functionRegex = /functions+w+s*(([^)]*))s*{([sS]*?)}/g; // Matches function definitions
    const blocks: { start: number; end: number; }[] = [];
    let match;
    while ((match = functionRegex.exec(code)) !== null) {
        const start = match.index;
        const end = match.index + match[0].length;
        blocks.push({ start, end });
    }
    return blocks;
}

This code defines a function findFoldableBlocks that takes the code as input and returns an array of objects, each containing the start and end positions of a foldable block (in this case, function definitions). The regular expression /functions+w+s*(([^)]*))s*{([sS]*?)}/g is used to find function definitions. Let’s break it down:

  • functions+: Matches the keyword “function” followed by one or more whitespace characters.
  • w+: Matches one or more word characters (the function name).
  • s*(([^)]*))s*: Matches the function’s parameter list enclosed in parentheses, allowing for whitespace.
  • {: Matches the opening curly brace of the function body.
  • ([sS]*?): Matches the function body, including any characters (including newlines) lazily.
  • }: Matches the closing curly brace of the function body.
  • g: The global flag, to find all matches in the code.

2. Rendering Folding Indicators

We’ll add a visual indicator (e.g., a plus or minus sign) next to the lines where folding is possible. This involves the following steps:

  • Analyzing the code to identify foldable lines.
  • Inserting the folding indicator elements into the DOM.
  • Styling the indicators.

Modify src/index.ts to include rendering folding indicators:


// src/index.ts

interface FoldableBlock {
    start: number;
    end: number;
    collapsed: boolean;
}

const codeEditor = document.getElementById('code-editor') as HTMLTextAreaElement;
let foldableBlocks: FoldableBlock[] = [];

function findFoldableBlocks(code: string): FoldableBlock[] {
    const functionRegex = /functions+w+s*(([^)]*))s*{([sS]*?)}/g;
    const blocks: FoldableBlock[] = [];
    let match;
    while ((match = functionRegex.exec(code)) !== null) {
        const start = match.index;
        const end = match.index + match[0].length;
        blocks.push({ start, end, collapsed: false });
    }
    return blocks;
}

function renderFoldingIndicators() {
    if (!codeEditor) return;
    const code = codeEditor.value;
    foldableBlocks = findFoldableBlocks(code);

    // Clear existing indicators
    const existingIndicators = document.querySelectorAll('.fold-indicator');
    existingIndicators.forEach(indicator => indicator.remove());

    foldableBlocks.forEach(block => {
        const lineNumber = getLineNumberFromPosition(code, block.start);
        if (lineNumber === null) return;
        const lineStart = getLineStart(code, lineNumber);

        if (lineStart === null) return;

        const indicator = document.createElement('span');
        indicator.classList.add('fold-indicator');
        indicator.dataset.start = String(block.start);
        indicator.dataset.end = String(block.end);
        indicator.dataset.collapsed = String(block.collapsed);
        indicator.textContent = block.collapsed ? '+' : '-';
        indicator.style.cursor = 'pointer';
        indicator.style.marginLeft = '5px';

        // Get the line element to append the indicator to.  This is a simplification
        // and won't work perfectly, but gives us a visual.
        const lineElement = getLineElement(lineNumber);
        if (lineElement) {
            lineElement.prepend(indicator);
        }
    });
}

function getLineNumberFromPosition(code: string, position: number): number | null {
    const lines = code.substring(0, position).split('n');
    if (lines.length === 0) return null;
    return lines.length;
}

function getLineStart(code: string, lineNumber: number): number | null {
    const lines = code.split('n');
    if (lineNumber  lines.length) return null;
    let start = 0;
    for (let i = 0; i < lineNumber - 1; i++) {
        start += lines[i].length + 1; // +1 for the newline character
    }
    return start;
}

function getLineElement(lineNumber: number): HTMLElement | null {
    // This is a simplification.  In a real editor, you'd have a much more
    // sophisticated way of mapping line numbers to DOM elements.
    const lines = codeEditor.value.split('n');
    if (lineNumber  lines.length) return null;
    // In a real editor, you would likely have a better way to get the line element.
    // This is a simplified approach, assuming each line is somehow represented
    // within the DOM (e.g., a <pre> or <div> for each line).
    // This is a placeholder and won't work perfectly without more DOM structure.
    const lineElement = document.createElement('div'); // Placeholder, replace with actual line element
    lineElement.textContent = lines[lineNumber - 1];
    return lineElement;
}


codeEditor.addEventListener('input', renderFoldingIndicators);

// Initial rendering
renderFoldingIndicators();

In this code:

  • We’ve added a FoldableBlock interface to store the start, end, and collapsed state of each block.
  • The renderFoldingIndicators function is responsible for finding foldable blocks, inserting the indicators into the DOM, and setting up click event listeners.
  • getLineNumberFromPosition gets the line number of a given character position.
  • getLineStart calculates the starting character position of a given line number.
  • getLineElement is a placeholder to get the element associated with a line number. This is simplified, and in a real editor, you’d have a more sophisticated mapping.
  • The input event listener on the codeEditor calls renderFoldingIndicators whenever the content of the editor changes.

Also, add the following styles to src/style.css to style the folding indicators:

.fold-indicator {
    margin-right: 5px;
    cursor: pointer;
    user-select: none;  /* Prevent text selection */
}

3. Handling Click Events

When a user clicks on an indicator, we need to toggle the visibility of the corresponding code block. This involves:

  • Adding event listeners to the indicators.
  • Updating the DOM to hide or show the code.
  • Updating the indicator’s state (e.g., changing the plus/minus sign).

Modify src/index.ts to handle click events on the indicators:


// src/index.ts
// ... (previous code)

function renderFoldingIndicators() {
    if (!codeEditor) return;
    const code = codeEditor.value;
    foldableBlocks = findFoldableBlocks(code);

    // Clear existing indicators
    const existingIndicators = document.querySelectorAll('.fold-indicator');
    existingIndicators.forEach(indicator => indicator.remove());

    foldableBlocks.forEach(block => {
        const lineNumber = getLineNumberFromPosition(code, block.start);
        if (lineNumber === null) return;
        const lineStart = getLineStart(code, lineNumber);

        if (lineStart === null) return;

        const indicator = document.createElement('span');
        indicator.classList.add('fold-indicator');
        indicator.dataset.start = String(block.start);
        indicator.dataset.end = String(block.end);
        indicator.dataset.collapsed = String(block.collapsed);
        indicator.textContent = block.collapsed ? '+' : '-';
        indicator.style.cursor = 'pointer';
        indicator.style.marginLeft = '5px';

        indicator.addEventListener('click', (event) => {
            const start = parseInt(indicator.dataset.start || '0', 10);
            const end = parseInt(indicator.dataset.end || '0', 10);
            toggleBlockVisibility(start, end);
            renderFoldingIndicators(); // Re-render indicators after toggling
        });

        // Get the line element to append the indicator to.  This is a simplification
        // and won't work perfectly, but gives us a visual.
        const lineElement = getLineElement(lineNumber);
        if (lineElement) {
            lineElement.prepend(indicator);
        }
    });
}

function toggleBlockVisibility(start: number, end: number) {
    foldableBlocks = foldableBlocks.map(block => {
        if (block.start === start && block.end === end) {
            return { ...block, collapsed: !block.collapsed };
        } 
        return block;
    });

    const code = codeEditor.value;
    const lines = code.split('n');

    // This is a very basic example of hiding/showing the block.  In a real editor,
    // you'd manipulate the DOM elements directly.
    let hiddenCode = '';
    let currentPos = 0;
    for (const block of foldableBlocks) {
        if (block.collapsed) {
            hiddenCode += code.substring(currentPos, block.start);
            hiddenCode += '// ...n'; // Or some other placeholder
            currentPos = block.end;
        } else {
          //  hiddenCode += code.substring(currentPos, block.end) + 'n';
        }
    }
    hiddenCode += code.substring(currentPos);
    codeEditor.value = hiddenCode;
}

// ... (rest of the code)

In this code:

  • We add a click event listener to each indicator within the renderFoldingIndicators function.
  • The toggleBlockVisibility function is called when an indicator is clicked. It updates the foldableBlocks array to reflect the collapsed state and modifies the editor’s content based on the collapsed state.
  • We re-render the indicators after toggling, to update the plus/minus signs.

4. Building and Running the Code

To compile and run your code, use the following commands:

tsc

This command compiles your TypeScript code to JavaScript and places the output in the dist directory. Then, open index.html in your browser. You should see the code editor with folding indicators next to function definitions. Clicking the indicators will toggle the visibility of the function bodies (although, the collapsing implementation is basic).

Advanced Features and Enhancements

The code editor we’ve built is a basic implementation. To make it more robust, you can add several advanced features and enhancements:

  • More Sophisticated Folding Detection: Implement folding for other code structures such as loops (for, while), conditional statements (if, else), and nested blocks. Consider using a parser (e.g., Acorn, Babel) for more accurate and reliable folding.
  • Line Numbering: Add line numbers to the code editor to improve readability and make it easier to navigate the code.
  • Syntax Highlighting: Implement syntax highlighting to make the code more visually appealing and easier to understand. Libraries like Prism.js or highlight.js can be used to achieve this.
  • Code Indentation: Automatically handle code indentation when the user presses the Tab key or when new lines are created.
  • Real-time Updates: Use a library like CodeMirror or Monaco Editor to create a more feature-rich and performant code editor. These libraries offer advanced features like syntax highlighting, code completion, and more.
  • Persistent State: Allow users to save their editor state (including folded/unfolded blocks) using local storage or a backend database.

Common Mistakes and Troubleshooting

Here are some common mistakes and troubleshooting tips:

  • Incorrect File Paths: Double-check the file paths in your index.html and tsconfig.json files to ensure that the TypeScript compiler and the browser can locate the necessary files.
  • TypeScript Compilation Errors: Review the output of the TypeScript compiler (tsc command) for any errors. Common errors include type mismatches, syntax errors, and missing dependencies.
  • DOM Manipulation Issues: When manipulating the DOM, make sure you’re selecting the correct elements and updating them correctly. Use the browser’s developer tools to inspect the DOM and identify any issues.
  • Regular Expression Issues: Regular expressions can be tricky. Test your regular expressions thoroughly to ensure they correctly identify the code blocks you want to fold. Use an online regex tester to debug the expressions.
  • Event Listener Issues: Ensure that event listeners are correctly attached to the elements and that the event handlers are being called as expected. Use console.log statements to debug event handling.

Key Takeaways

  • TypeScript allows us to build powerful and maintainable web applications.
  • Code folding significantly improves code readability and navigation.
  • Regular expressions can be used to identify foldable blocks.
  • DOM manipulation is essential for rendering and updating the code editor.
  • Event listeners are used to handle user interactions.

FAQ

Here are some frequently asked questions about building a web-based code editor with code folding:

  1. What are the benefits of code folding? Code folding improves code readability, reduces visual clutter, and makes it easier to navigate large codebases.
  2. What are the different types of code blocks that can be folded? Common types of foldable code blocks include functions, loops, conditional statements, and code blocks within curly braces.
  3. How can I improve the accuracy of code folding? Use a parser (e.g., Acorn, Babel) to accurately identify code blocks.
  4. What are some popular libraries for building web-based code editors? Popular libraries include CodeMirror and Monaco Editor.
  5. How can I add syntax highlighting to my code editor? Use libraries like Prism.js or highlight.js.

Building a web-based code editor with code folding is a rewarding project that combines web development fundamentals with practical problem-solving. By following this tutorial, you’ve learned how to implement a basic code folding feature using TypeScript, HTML, and CSS. The provided code gives a foundation, and the advanced features are a great way to expand your skills. Remember that the presented code is a simplified implementation. The concepts can be extended to create complex, feature-rich code editors. The journey of building a code editor is an excellent way to learn about web development and TypeScript. By practicing and experimenting with the code, you’ll gain a deeper understanding of web development and TypeScript. As you expand your code editor with more features, you will gain a better grasp of DOM manipulation, event handling, and the overall architecture of web applications. Continue to explore, experiment, and learn. The world of web development is ever-evolving, and there is always something new to discover.