Building Code Editor
Though the title says "Building Code Editor," we would do more of an "Assembling Code Editor."
Code Editors are extremely challenging to build from scratch, and there is no reason for anyone to create something like that from scratch. There are three great open-source options:
-
Ace: The editor that powers Cloud9 IDE. It is a great editor that powers many online code editors. It is a bit old and not as actively maintained as it used to be.
-
Monaco Editor: The editor that powers VSCode. This course will use the React wrapper.
-
CodeMirror: A minimalist code editor that is highly extensible. We used this (in v1, now deprecated) on the Frontend Hire coding workspace.
All the editors are great, and you can use any of them. This course will use Monaco Editor. We used CodeMirror on the Frontend Hire (in v1, now deprecated) coding workspace because it is lightweight and feels cleaner for practicing coding. We did not use Ace to have much of an opinion on it.
You can read more about the differences between the editors Ace, CodeMirror, and Monaco: A Comparison of the Code Editors You Use in the Browser. It is an excellent comparison from Faris Masad at Replit.
Setting Up Monaco Editor
As mentioned, we will use the React wrapper for Monaco Editor.
Let us install it:
Let us create a component called CodeEditor.tsx
to render the code editor and other things like the files tab. We will place this component in the components
folder inside its folder CodeEditor
, do a barrel export, and render it in our App.tsx
. Joshua Comeau's Delightful React File Structure inspired this structure.
We will now render the Monaco Editor in the CodeEditor
component.
The Editor
component takes a theme
prop. We set it to vs-dark
to use the dark theme. You can set it to light
or omit it for the light theme.
The editor now renders, but we must use the multi-modal editor to support different files and languages. Before we do that, let us think about how we want to set up our code template.
Thinking our Code Template
WebContainer API has its own way of working with files and folders; ideally, we would like to use a structure that mimics it. We will do the refactor in the next section when we integrate the WebContainer API. We know this because we did a POC with the API and have read the documentation well enough. We are not doing it now because the code editor is a complex component, and we want to understand it first before adding the WebContainer API.
In this course, we will only support React with Vite support. The files would take a top-level structure with the following files:
App.jsx
: The main React component.index.jsx
: The entry file that renders theApp
component.index.html
: The HTML file that loads the React app.package.json
: The package file with the dependencies.vite.config.js
: The Vite configuration file.
We can use an object to represent the files. The keys would be the file names, and the value would be another object holding more information about the file, such as the contents. We could add more properties like readOnly
and hidden
to control the file's behavior in the editor. But for now, let us keep it simple with just the contents
property.
The types would look like this:
We use CodeFile instead of File to represent the file to avoid conflicts with the File Interface in JavaScript. We use a Template to describe the code template. We are also not using an array for the files because we want to access the files by their names.
Next up is the actual template. We will create a templates
folder in the src
folder and place the template in a file called react-vite.ts
along with the types.
Now, let us use the template and try to populate our multi-modal editor.
The path
prop is what enables this multi-modal editor. It tells the editor which files it is editing. The defaultValue
prop sets the initial value of the editor. We use the activeFile
state to keep track of the active file. We set it to the entry
property of the template to start with the entry file.
We also derive the currentFile
from the activeFile
to get the current file's contents. We use this to set the editor's default (initial) value. But how do we switch between files? We will add a files tab to the editor's top to switch between files.
Adding Files Tab
This files tab will be a simple list of files we can click to switch between files. We will use the visibleFiles
property of the template to determine which files are visible. For the sake of simplicity, we will use a button
element to represent each file. We will also add some styles to make it look like a tab. We will use the activeFile
state to determine which file is active.
We can now render the respective file when we click the file tab. We can also see the active file highlighted. We can now switch between files.
Clean up the code by extracting the file tab into its component.
We will create a FileTabs
component in a single file, FileTabs.tsx
, in the components/CodeEditor
folder. We will also add some styles to make it look better and handle a bit of overflow.
We can now use this component in the CodeEditor
component.
Great, we should also use better logic to handle conditional classes. The best way to merge the classes is to use clsx
and tailwind-merge
.
clsx
helps with the conditional stuff, while tailwind-merge
helps merge the classes.
Let us install them:
We will create a utility function to handle these classes.
Let's use this utility function in the FileTabs
component.
One more important thing to do is to handle the editor's language. We can use the file extension to determine the language. Monaco Editor supports many languages; we can use the defaultLanguage
prop to set the language. We will use the activeFile
state to get the file extension and set the language accordingly.
Let us create a utility function to get the language from the file extension.
And use it in the CodeEditor
component.
These are the basics of building a code editor. We have a multi-modal editor that can switch between files and set the language based on the file extension. We also have a file tab to switch between files.
What is the difference between value
and defaultValue
? Similarly, what is
the difference between language
and defaultLanguage
, path
and
defaultPath
? The default
prefix is used to set the initial value,
language, path of the editor during its creation. The value
, language
, and
path
props are used to update the editor's value, language, and path after
the editor has been created. In most cases, you can use the value
,
lanuage
, and path
props instead of the default
ones. But for multi-modal
editor, it would be best to use the default
ones to set the initial values
except for the path. Otherwise, your changes will be lost when you switch
between the files.
In the next section, we will focus on the following core feature of our project: the integration of the WebContainer API.
At this point, our code should match the code in the branch 3-monaco-editor
.