In the world of web development, code editors are indispensable tools. They help us write, edit, and debug code efficiently. But what if you could build your own, tailored to your specific needs? This tutorial will guide you through creating a simple, yet functional, web-based code editor using TypeScript. We’ll focus on a key feature that significantly boosts developer productivity: code completion.
Why Build a Code Editor?
You might be wondering, “Why build a code editor when there are so many excellent options available?” Here’s why:
- Customization: You can tailor the editor to your exact requirements, adding features and functionalities not found in standard editors.
- Learning: Building a code editor is an excellent way to deepen your understanding of programming concepts, such as parsing, syntax highlighting, and language services.
- Portability: A web-based editor can be accessed from any device with a web browser, making it ideal for collaborative coding or on-the-go development.
- Understanding Language Services: Code completion relies heavily on understanding how programming languages work. Building your own gives you insight into the tools that power modern IDEs.
What We’ll Cover
This tutorial will cover the following key aspects:
- Setting up a basic TypeScript project.
- Creating a simple text editor interface with HTML and CSS.
- Implementing basic syntax highlighting.
- Implementing code completion suggestions.
- Integrating with a language service (for suggestions).
- Handling user input and editor interactions.
Prerequisites
Before you start, you’ll need the following:
- Basic understanding of HTML, CSS, and JavaScript: You should be familiar with the fundamentals of web development.
- Node.js and npm (or yarn) installed: These are required for managing project dependencies and running the TypeScript compiler.
- A code editor: Choose your favorite code editor (VS Code, Sublime Text, Atom, etc.).
- TypeScript knowledge: While we’ll explain concepts as we go, some familiarity with TypeScript will be helpful.
Setting Up the Project
Let’s start by setting up our project. Open your terminal and create a new project directory:
mkdir typescript-code-editor
cd typescript-code-editor
Initialize a new npm project:
npm init -y
Install TypeScript as a development dependency:
npm install typescript --save-dev
Create a `tsconfig.json` file in your project root. This file configures the TypeScript compiler. Here’s a basic configuration:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}
This configuration compiles TypeScript to ES5 JavaScript, uses CommonJS modules, and outputs the compiled files to a `dist` directory. The `strict: true` option enables strict type checking, which is highly recommended for writing robust TypeScript code. The `esModuleInterop: true` flag helps with compatibility when importing ES modules in CommonJS.
Create a `src` directory and a `main.ts` file inside it:
mkdir src
touch src/main.ts
Creating the HTML and CSS
Next, let’s create the HTML and CSS for our code editor’s user interface. Create an `index.html` file in the project root:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TypeScript Code Editor</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="editor-container">
<textarea id="editor"></textarea>
</div>
<script src="dist/main.js"></script>
</body>
</html>
This HTML provides the basic structure: a container for the editor and a `textarea` element where the code will be entered. It also links to a `style.css` file for styling and includes the compiled `main.js` file.
Create a `style.css` file in the project root:
.editor-container {
width: 80%;
height: 500px;
margin: 20px auto;
border: 1px solid #ccc;
border-radius: 5px;
overflow: hidden;
}
#editor {
width: 100%;
height: 100%;
padding: 10px;
font-family: monospace;
font-size: 14px;
border: none;
outline: none;
resize: none;
background-color: #f9f9f9;
}
This CSS styles the editor container and the `textarea` to give it a basic look and feel. The `font-family: monospace;` setting is crucial for displaying code consistently.
Implementing Basic Syntax Highlighting
Syntax highlighting makes code easier to read by coloring different parts of the code based on their meaning (keywords, comments, strings, etc.). Let’s add basic syntax highlighting to our editor. This is a simplified approach, but it demonstrates the core principles.
Modify `src/main.ts`:
const editor = document.getElementById('editor') as HTMLTextAreaElement;
// Define a simple highlighting function
function highlight(code: string): string {
// Keywords to highlight
const keywords = ['function', 'const', 'let', 'var', 'if', 'else', 'return', 'class', 'import', 'from'];
// Regex for comments
const commentRegex = /(//.*|/*[sS]*?*/)/g;
// Regex for strings
const stringRegex = /"([^"\]*(\.[^"\]*)*)"|'([^'\]*(\.[^'\]*)*)'/g;
// Highlight comments
code = code.replace(commentRegex, '<span style="color: green;">$1</span>');
// Highlight strings
code = code.replace(stringRegex, '<span style="color: brown;">$1</span>');
// Highlight keywords
keywords.forEach(keyword => {
const regex = new RegExp(`b(${keyword})b`, 'g'); // Ensure whole words
code = code.replace(regex, '<span style="color: blue; font-weight: bold;">$1</span>');
});
return code;
}
function updateHighlighting() {
if (!editor) return;
const code = editor.value;
const highlightedCode = highlight(code);
// Replace the content with highlighted code
// This is a simplified approach. For better performance, consider using a separate element
// or a library like Prism.js or highlight.js
editor.innerHTML = highlightedCode;
}
// Listen for input changes
if(editor) {
editor.addEventListener('input', () => {
updateHighlighting();
});
}
// Initial highlighting
if(editor) {
updateHighlighting();
}
This code does the following:
- Gets the `textarea` element.
- Defines a `highlight` function that takes code as input and returns the highlighted HTML.
- Inside `highlight`, it defines arrays and regex expressions for keywords, comments, and strings.
- Uses `replace` to wrap the matching code with `span` elements, applying inline styles for color. Note: This is a simplified example. For complex highlighting, you’ll want a more robust solution, such as using a dedicated syntax highlighting library like Prism.js or highlight.js.
- The `updateHighlighting` function gets the text from the editor, runs it through the `highlight` function, and updates the `innerHTML` of the editor. Important: This approach can be inefficient for large code blocks. Consider using a separate element (like a `div` with `contenteditable=”true”`) for better performance.
- Adds an event listener to the `textarea` to trigger highlighting on input.
- Calls `updateHighlighting()` initially to highlight any existing code.
Now, compile your TypeScript code:
tsc
Open `index.html` in your browser. You should see basic syntax highlighting as you type.
Implementing Code Completion
Code completion (also known as autocompletion or intellisense) is a powerful feature that suggests possible code elements (variables, functions, etc.) as the user types. This section will guide you through adding this functionality to your editor. We will use a simplified approach to demonstrate the core concepts. For more advanced features, you’d typically integrate with a Language Server Protocol (LSP) server.
Modify `src/main.ts`:
const editor = document.getElementById('editor') as HTMLTextAreaElement;
const suggestionsContainer = document.createElement('div');
suggestionsContainer.id = 'suggestions-container';
suggestionsContainer.style.position = 'absolute';
suggestionsContainer.style.backgroundColor = 'white';
suggestionsContainer.style.border = '1px solid #ccc';
suggestionsContainer.style.borderRadius = '5px';
suggestionsContainer.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)';
suggestionsContainer.style.zIndex = '10';
suggestionsContainer.style.display = 'none'; // Initially hidden
// Append suggestions container to the body
document.body.appendChild(suggestionsContainer);
// Sample suggestions (replace with dynamic data)
const sampleSuggestions = ['function', 'const', 'let', 'var', 'if', 'else', 'return', 'class', 'import', 'from'];
function showSuggestions(suggestions: string[], x: number, y: number) {
suggestionsContainer.innerHTML = '';
suggestions.forEach(suggestion => {
const suggestionElement = document.createElement('div');
suggestionElement.textContent = suggestion;
suggestionElement.style.padding = '5px';
suggestionElement.style.cursor = 'pointer';
suggestionElement.addEventListener('click', () => {
// Insert the suggestion into the editor
if (editor) {
const cursorPos = editor.selectionStart;
if(cursorPos !== null) {
const editorValue = editor.value;
const beforeCursor = editorValue.substring(0, cursorPos);
const afterCursor = editorValue.substring(cursorPos);
const lastWordStart = beforeCursor.lastIndexOf(' ') + 1;
const wordToReplace = beforeCursor.substring(lastWordStart);
const newValue = beforeCursor.substring(0, lastWordStart) + suggestion + afterCursor;
editor.value = newValue;
editor.selectionStart = cursorPos + suggestion.length - wordToReplace.length;
editor.selectionEnd = editor.selectionStart;
updateHighlighting(); // Re-highlight after insertion
}
}
suggestionsContainer.style.display = 'none'; // Hide suggestions after selection
});
suggestionsContainer.appendChild(suggestionElement);
});
suggestionsContainer.style.left = `${x}px`;
suggestionsContainer.style.top = `${y + 20}px`; // Position below the cursor
suggestionsContainer.style.display = 'block';
}
function hideSuggestions() {
suggestionsContainer.style.display = 'none';
}
function getSuggestions(text: string): string[] {
const lowerCaseText = text.toLowerCase();
return sampleSuggestions.filter(suggestion => suggestion.toLowerCase().startsWith(lowerCaseText));
}
// Listen for input changes in the editor
if (editor) {
editor.addEventListener('input', () => {
updateHighlighting(); // Re-highlight after input
const cursorPos = editor.selectionStart;
if(cursorPos !== null) {
const editorValue = editor.value;
const beforeCursor = editorValue.substring(0, cursorPos);
const lastSpaceIndex = beforeCursor.lastIndexOf(' ');
const lastWord = beforeCursor.substring(lastSpaceIndex + 1);
if (lastWord.length > 0) {
const suggestions = getSuggestions(lastWord);
if (suggestions.length > 0) {
const rect = editor.getBoundingClientRect();
const x = rect.left + editor.offsetLeft + (cursorPos - lastSpaceIndex - 1) * 8; // Adjust position
const y = rect.top + editor.offsetTop + 16; // Adjust position
showSuggestions(suggestions, x, y);
} else {
hideSuggestions();
}
} else {
hideSuggestions();
}
}
});
// Hide suggestions when the editor loses focus
editor.addEventListener('blur', () => {
setTimeout(() => {
hideSuggestions();
}, 100); // Small delay to prevent immediate hiding on click
});
// Hide suggestions if clicking outside
document.addEventListener('click', (event) => {
if (!suggestionsContainer.contains(event.target as Node) && event.target !== editor) {
hideSuggestions();
}
});
}
This code does the following:
- Creates a `suggestionsContainer` element that will hold the suggestions. It sets up basic styling for positioning and visibility.
- Appends the `suggestionsContainer` to the `document.body`.
- Defines `sampleSuggestions`, which contains a simple array of strings that will act as our code completion suggestions. In a real-world scenario, you’d fetch these suggestions from a language service or other data source.
- The `showSuggestions` function takes an array of suggestions, calculates the position (x, y) relative to the editor, and displays the suggestions in the `suggestionsContainer`. It also attaches a click event listener to each suggestion to insert it into the editor when clicked.
- The `hideSuggestions` function simply hides the suggestion container.
- The `getSuggestions` function filters the `sampleSuggestions` based on the text entered by the user.
- The input event listener in the editor now does the following:
- Retrieves the current cursor position.
- Determines the last word entered by the user.
- Calls `getSuggestions` to get relevant suggestions.
- If suggestions are found, it calls `showSuggestions` to display them. Note: The position calculation is basic and might need adjustments depending on your specific CSS and editor setup.
- If no suggestions are found, it hides the suggestion container.
- Adds a blur event listener to the editor to hide suggestions when the editor loses focus and a click event listener to the document to hide suggestions if the user clicks outside of the editor and suggestions container.
Compile the code again:
tsc
Refresh your `index.html` in your browser. Now, as you type, you should see code completion suggestions appear. Clicking on a suggestion should insert it into the editor.
Integrating with a Language Service (Advanced)
The code completion we implemented is based on a static list of suggestions. A real-world code editor leverages a language service, which provides more intelligent and context-aware code completion. Language services understand the syntax and semantics of the programming language, providing accurate suggestions based on the current code and context.
To integrate with a language service, you’d typically:
- Choose a Language Service: For TypeScript, you can leverage the built-in TypeScript Language Service. Other languages have their own services (e.g., for JavaScript, you might use a service that supports JSDoc).
- Establish Communication: You’ll need a way to communicate with the language service. This often involves using the Language Server Protocol (LSP). LSP defines a standard protocol for communication between code editors and language servers.
- Send Requests: When the user types, send requests to the language service (e.g., “getCompletionItems”) with information about the current code, cursor position, and context.
- Process Responses: Receive the suggestions from the language service and display them in your editor.
Implementing LSP is beyond the scope of this tutorial, but here are some libraries that can help:
- vscode-languageserver-protocol: Provides the protocol definitions and helper functions for implementing language servers and clients.
- vscode-languageserver: Provides utilities for building language servers.
- monaco-editor: Microsoft’s code editor used in VS Code. It includes built-in support for language services and LSP. (This is a much more complex solution, but it’s a powerful one).
Handling User Input and Editor Interactions
Our editor currently only responds to basic input. To make it more user-friendly, you can implement features such as:
- Keyboard Navigation: Allow users to navigate through the suggestions using the up and down arrow keys and select a suggestion with the Enter key.
- Tab Completion: Allow users to select the first suggestion by pressing the Tab key.
- Undo/Redo: Implement undo and redo functionality using the `history` API.
- Syntax Highlighting on the Fly: Update the syntax highlighting dynamically as the user types.
- Error Highlighting: Integrate with a language service or a linter to highlight syntax errors and other issues.
- Code Formatting: Integrate a code formatter (e.g., Prettier) to automatically format the code.
Here’s how you could add keyboard navigation to the suggestions:
Modify `src/main.ts`:
// ... (previous code) ...
let selectedSuggestionIndex = -1;
function showSuggestions(suggestions: string[], x: number, y: number) {
// ... (previous code)
selectedSuggestionIndex = -1;
suggestions.forEach((suggestion, index) => {
// ... (previous code)
if (index === selectedSuggestionIndex) {
suggestionElement.style.backgroundColor = '#ddd'; // Highlight the selected item
}
});
suggestionsContainer.addEventListener('keydown', (event) => {
if (event.key === 'ArrowDown') {
event.preventDefault(); // Prevent cursor movement in the editor
selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, suggestions.length - 1);
highlightSelectedSuggestion();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, 0);
highlightSelectedSuggestion();
} else if (event.key === 'Enter') {
event.preventDefault();
if (selectedSuggestionIndex !== -1) {
// Simulate a click on the selected suggestion
(suggestionsContainer.children[selectedSuggestionIndex] as HTMLElement).click();
}
} else if (event.key === 'Escape') {
hideSuggestions();
}
});
suggestionsContainer.focus(); // Give focus to the container to allow keyboard events
}
function highlightSelectedSuggestion() {
for (let i = 0; i < suggestionsContainer.children.length; i++) {
const suggestionElement = suggestionsContainer.children[i] as HTMLElement;
if (i === selectedSuggestionIndex) {
suggestionElement.style.backgroundColor = '#ddd';
} else {
suggestionElement.style.backgroundColor = 'white';
}
}
}
// ... (rest of the code) ...
This adds the following:
- `selectedSuggestionIndex`: A variable to keep track of the currently selected suggestion.
- Keydown event listener: Adds a keydown event listener to the `suggestionsContainer` to handle arrow key, Enter, and Escape key presses.
- `highlightSelectedSuggestion()` function: Highlights the currently selected suggestion.
- The suggestionsContainer is given focus when displayed, so that keydown events will be captured.
- Inside `showSuggestions`, the selection index is reset.
Remember to compile your code after making these changes (`tsc`).
Common Mistakes and How to Fix Them
Here are some common mistakes and how to fix them when building a code editor:
- Incorrect Syntax Highlighting: The regex expressions for syntax highlighting can be tricky. Test your regex thoroughly and ensure they correctly match the code elements you want to highlight. Use online regex testers to help.
- Performance Issues: Updating the DOM (e.g., `innerHTML`) excessively, especially for large code files, can cause performance problems. Consider using techniques like virtual DOM or dedicated syntax highlighting libraries.
- Incorrect Positioning of Suggestions: The positioning of the suggestions container might be incorrect. Carefully calculate the `x` and `y` coordinates based on the editor’s dimensions, scroll position, and cursor position. Use the browser’s developer tools to inspect the positioning.
- Incomplete Code Completion Logic: The code completion logic might not handle all cases correctly. Consider edge cases, such as code completion within strings, comments, and nested structures.
- Lack of Error Handling: Add error handling to gracefully handle unexpected situations (e.g., network errors when fetching suggestions from a language service).
- Ignoring Accessibility: Ensure your code editor is accessible to users with disabilities. Use semantic HTML, provide ARIA attributes, and ensure keyboard navigation works correctly.
Key Takeaways
- Building a web-based code editor in TypeScript can be a rewarding learning experience.
- Syntax highlighting and code completion significantly enhance the coding experience.
- Implementing code completion involves understanding text input, filtering suggestions, and dynamically displaying them.
- Integrating with a language service provides more advanced and context-aware code completion.
- Consider performance, user experience, and accessibility when building your editor.
FAQ
Q: How can I improve the performance of syntax highlighting?
A: Consider using a dedicated syntax highlighting library like Prism.js or highlight.js, or implement a virtual DOM approach to update only the necessary parts of the code.
Q: How can I handle different programming languages?
A: You’ll need to define different rules (regex, keywords) for each language. You can use a configuration file or a language-specific module to manage these rules. For a more sophisticated approach, you can integrate with language services for different languages.
Q: How can I add support for code formatting?
A: Integrate a code formatter like Prettier. You can call the formatter’s API to format the code when the user saves the file or presses a specific key combination.
Q: How can I add support for themes?
A: Allow users to select a theme (light, dark, etc.). Then, dynamically update the CSS styles applied to the editor based on the selected theme. You can use CSS variables for easy theming.
Next Steps
Building a code editor is a complex project, and this tutorial provides a basic foundation. To take your editor to the next level, consider exploring advanced features like:
- Integrating with a Language Server Protocol (LSP) server for more accurate and comprehensive code completion, error checking, and other language-specific features.
- Adding support for code folding and outlining.
- Implementing a debugger.
- Adding support for multiple files and projects.
With the knowledge gained from this tutorial, you are well on your way to creating a powerful and customized code editor. Remember, the journey of building a code editor involves continuous learning and experimentation. Embrace the challenges, and enjoy the process of creating a tool that can significantly improve your coding productivity.
