In this chapter, we're going to implement the code editor as well as state management of our files. We're going to do that by setting up Sustand state management, installing and configuring CodeMirror 6, setting up OneDark theme configuration, building a tabbed file switcher component, and adding syntax highlighting for languages. So this is how that's going to look like. This is the finished project. In the previous chapter we actually implemented the file explorer, but what we didn't implement is the logic of what file is selected.
And we didn't implement the ability to see the contents of that file instead of a code editor which has the proper syntax highlighting for its file. So the first thing we're going to do is we're going to implement the state management. As you can see it has a very specific temporary state, right? So right now when I switch between different files you can see that it's temporary. Unless I double click, then it becomes permanent.
And when I click on the next one, that one becomes temporary. Instead of just opening a bunch of tabs. I found this behavior from the actual VS Code. You can see that right now I have this file opened, create input, but when I click on another one, it doesn't open a new tab, it just replaces the existing one. Until I double click.
And then you can see it starts opening multiple tabs. So I borrowed the behavior from the original editor because I think it's a very good implementation so I want to bring it to our editor as well. So without further ado, let's get started on that. So I'm going to go ahead and do npm run dev and we're going to start by installing zustand. So let's go ahead and do npm install zustand.
I'm going to show you the version that I'm using immediately, even though very rarely have I heard of any breaking changes inside of Zustand. State management is kind of solved, you know. So let's go ahead and go inside of the following. I want to kind of prepare source, features, projects, components, file explorer tree. So this specific file.
The reason is because this file is almost finished besides a few things. And those few things are, let me find it, here they are. OnClick is empty. OnDoubleClick is also empty. ToDo, CloseTab, right?
We have kind of this leftover things that we just left here, right? So that's what we're going to be able to wrap up and forever finish this tree component. We first have to implement the state management. So let's go ahead and do that. I'm gonna go inside of Source, Features, Projects, and let's create a new folder called Store.
And inside, use editor-store.ts. Let's import create from zustand. Let's import the id from convex generated data model. Now let's create an interface called tab state. It's gonna have an array of open tabs, which are just a bunch of id files, and active tab id and preview tab id, which are the same type but optional.
Now let's go ahead and according to that interface create a default tab state and give it the proper interface. Empty array and null null as the initial values. Now let's go ahead and let's open interface editor store. Inside of here I want to start with tabs. So tabs are going to be a map and we're going to give it a type, first arguments id of projects and the second one tab state.
Those are going to be the tabs. Then let's go ahead and let's open get, let's create get tab state, which accepts the project id, which is this type and simply returns tab state. Then let's go ahead and create open file. Open file is a function which accepts project ID, file ID, options in which we have one property pinned. That is the functionality I was talking to you about.
Will it persist or will it replace on next click? So we are just writing types for the editor store now. After open file, let's implement close tab, which accepts a project ID, a file ID and a void. Then let's go ahead and implement close all tabs, which accepts project ID and the void. So this will be per project, right?
And the last one we have is setActiveTab, which accepts a project ID and a file ID and returns a void. Let me expand this even more so perhaps you can see all of them in one line. Great. So now I'm going to remove the spaces between them. I just added them so it's easier for you to look at.
Now that we have the interface for the editor store, let's actually go ahead and develop. Export const useEditorStore. We're going to use the create method from zustand and let's give it an argument of editor store, the interface we've just created. Go ahead and execute it and then immediately execute again. And from here, open another parenthesis, so be mindful a lot of parenthesis happening here.
And again, open parenthesis and immediately return an object inside. Like this. And now in here, let's start defining things. So we're now going to have a bunch of errors until we populate all of these functions and types we've added. So let's start with the tabs.
That's going to be a new map. Then let's go ahead and let's implement getTabState, the second one. GetTabState will accept a project ID and it will return get, which is this method right here, .tabs.get, which is using get from the map JavaScript or JavaScript, what should I use, entity, I guess that's the word, or fallbacks to default tab state. We are using map because it doesn't allow duplicates. So it kind of does a lot of work for us in making sure no duplicate tabs are open.
Great, so that is getTabState. Now let's implement a complex one. Open file. It will accept a project ID, file ID, and last but not least, the options in which we have the pinned property. So let's go ahead and start defining the things we need.
First, let's get all the tabs using new map get tabs. Make sure to execute the get. Then, let's get the current state using tabs.get project ID. So, we are getting all the currently opened tabs for this project id, or we are defaulting to default tab state. From the state we can extract open tabs and preview tab ID.
And now we can check if this current file is already open. Do open tabs include the file ID we're attempting to open. Now let's have some scenarios. So first scenario, Opening as preview. So that is this.
When I click on something, you can see how it opens. It's not persistent because if I click something else, it changes. Right? So we call that preview. Not pinned.
Right? That is case 1. So let's go ahead and do an if check if it is not opened and we are not pinning it so if not open and not pinned let's go ahead and reinitialize new tabs with this new preview tab so preview tab id which we've destructured from the state let's check if we have it if we do call open tabs dot map find the ID open parentheses and check if ID is equal to preview tab ID then use the file ID we just passed here. Otherwise, just return the ID you found. And the alternative is to simply add to the list of open tabs a new file ID.
All right now here's the thing it's very annoying to look at these errors here so I'm thinking if I remove this does it make it easier to look at? Oh kind of but not too much. Unfortunately we're gonna have to go through this with a bunch of errors and then hopefully we won't have any at the end. It's just hard to define like did we make a mistake in the syntax or is it just a type error. But let's just continue for now.
Alright, so we've just successfully defined new tabs here. The only problem is we are not using it. So let's call tabs.setProjectID openTabs, newTabs activeTabID, fileID previewTabID, fileID So an active tab is the one the user is currently seeing in the code editor. The preview tab ID is referring to the top bar here. So a file can be both active but still in the preview phase.
For example, like this. It's active, but it's in preview, right? You can see it's italic. So that's the first scenario. And let's call set tabs and do an early return.
So that is case one finished. Make sure you've closed the if clause. Now let's do a second scenario which is much simpler. Case 2. Opening a tab immediately as pinned.
In that case all we have to do is just open a new tab. So if the tab is not open but we called it with a purpose to be pinned, which could be initiated through a double click, for example. Let's simply call tabs.set for this project id, spread the state, call open tabs, spread, open tabs, and simply add file id here, and set the file tab ID, my apologies, the active tab ID to the file ID. Then call set, put in the new tabs and do an early return. I think that maybe we can just every now and then remove the editor store, just to confirm you don't have any big syntax errors.
Okay, now that I look at it, it's definitely easier to look at without giving it the type. Like, okay, this we're going to see if this actually works later or not. You should have all of these errors. All of this is perfectly fine. It's mostly just type errors, right?
But it's way easier to look at, especially when building long ones like this. So that was case 2. Now let's go ahead and do case 3, which isn't going to be complicated. Case 3. The file is already open, but we attempted to open it again and pin if double-clicked.
So const should we pin it if we specifically gave it an option to pin or if preview tab ID is equal to file ID. So if we attempt to open a previewed file once more, obviously we want it to be pinned. So tabs.set, projectId, state, activeTabId, setToFileId, previewTabId. Let's go ahead and do it. Project ID, state, active tab ID, set to file ID, preview tab ID.
Let's go ahead and do. Should we pin it? If we should, null, otherwise keep it as previewTabId. And go ahead and set tabs. No need to do an early return because this is the last if clause.
Keep in mind if this is like super complicated, that's kind of okay. It's very hard to imagine how this actually works. But once we connect it to the UI, it will be easier. You can see that even I myself made these comments so it's easier to explain what we're doing here. So perfectly fine if you feel confused here.
It's a big chunk of logic and it will make more sense once we have the UI to go with it. So yes, don't worry. Let's go ahead and continue developing our methods here. So I'm going to add close tab method now, which will accept a project ID and a file ID, right? So again, let's start.
First we get the tabs using new map get.tabs. From that let's go ahead and get the state from the current project ID or fallback to the default state. We already know that from the state we can extract open tabs, active tab id and preview tab id. And now let's just wrap it up by adding a tab index from the open tabs. So we are attempting to find if the file id is one of the open tabs by using index off.
And we are going to use it later so the first thing we can check, if the tab index is minus 1, it means it's already closed we have nothing to close, so let's just do an early return Now let's go ahead and prepare how the new tabs are going to look like. So the new tabs will be openTabs.filter and basically just filter out the file ID that we have passed. So make sure you use the opposite logic here, right? Now let's go ahead and let's define the following new active tab ID. That's basically the complicated part here.
Once we close a tab, what should become the next active tab? We could just fall back to a blank page, but why should we? We can implement a very simple kind of decision of what should be the next tab. So if an active tab ID is equal to the file we have just closed, we have to do something. So first things, let's check.
Are the new tabs, meaning once we remove this file, are there any tabs left over? Because if there aren't, there's nothing we can do. Let's just go ahead and pass a new active tab ID to null. Alternatively, if we are able to find some other tabs, so else if tab index is greater or equal than the new tabs dot length, we are basically doing the logic. Should we make the next active tab, the one that was opened before this one, we just closed or after it?
So for this scenario, let's set the new active tab to be new tabs new tabs dot length minus one basically the last one we opened. Else let's just go ahead and fall back to new active tab ID and then to whatever tab index we just found. Why can we do that? Well, because this tab index is no longer this closed tab. So it's just the last one left, right?
Once we do that we can call tabs dot set project ID open tabs new tabs active tab ID new active tab ID preview tab ID preview tab ID equals to what we passed fileId null, otherwise previewTabId and finally let's set those tabs. There we go. And now we have a few very simple methods. So, the first one is to close all tabs in a project. We pass in the project ID, we call the tabs using new map and inside we execute get and we access the tabs property and we call tabs.setProjectID, simply default it to an empty array with no open tabs or anything like that.
And call a setter on that. And last one, setActiveTab for a project for this file. As always, let's go ahead and copy how we get the tabs like this const state tabs dot get for this project or default tab state if this project has no open tabs. Call tabs.set() and add to this project ID whatever is the current state and simply modify the current active tab ID to be file ID. And set tabs.
That's it. We are now ready to give this a proper type. So go to use editor store and give it the editor store. And if you've done it correctly, you shouldn't have any errors at all. So if you do have some errors, it's most likely a typo, right?
Because, for example, in things like this, if I do active tab ID 2 you can see it gives me an error but if I don't have editor store here I don't think it's gonna give me an error yeah you can see how it's completely allowing me to do that. So if you have some typo here, these adding editor store will help you do that. So yeah, you should definitely have no errors once you add the editor store. The editor store will ensure that you have complete type safety here. I'm slowly going to go through the entire file once again if you want to pause and if you want to check and compare.
Of course you can access the source code too. But still, if you want to do it this way, I'm just going over everything once again, so you can pause the screen and see. Great. Now let's go inside of the hooks and let's create useEditor.ts. And inside of useEditor, let's go ahead and export const use editor project id a type of id projects and let's go ahead and add some other imports.
We are going to need use callback from react and we are going to need use editor store. From store use editor store. Now I'm going to define the store here store from use editor store. Then I'm going to grab the tab state for my specific project ID. So tab state, use editor store, we get the entire state, and then we call getTabState, but just for this one project that I currently have opened, right?
This is the logic. Once I have the tab state, I can start developing things like open file. We're going to call use callback so we don't have any reactivity problems here. So what are the arguments we're going to pass? Well, the first argument is going to be the file ID.
And the second argument will be options, should we pin this file or not. And then the logic in here is actually very easy. Store open file, project ID, file ID and options and the dependency array should have store and project ID. So because we did all the complex logic inside of the actual store we don't have to do it here. In here, we just make sure that we can easily call this hook, use editor and pass in the project ID.
And we don't have to worry about passing project ID in every single store function. Because right now, take a look at all of these functions. They all accept project ID. That's very annoying, right? So we are creating an abstraction over that so that we kind of have easier and better developer experience when calling these functions.
Now same thing is true for close tab, for example. Close tab use callback which accepts a file ID and simply calls store close tab, passes in the project ID and the file ID. So the exact same thing is here, right? I tried to write it in the same way, but I think you get the idea, right? This first one is just the parameters of the function, right?
Okay, so that was close tab. Now let's go ahead and let's do close all tabs. Again, use callback. This time, no props at all. Just store close all tabs for this project.
And then let's go ahead and do the last one. Set active tab. Again, use callback, accepts a file ID, calls store set active tab project ID file ID and these two in the dependency array. Now let's go ahead and let's return all the things we're going to need. So we are going to map tabstate.opentabs to just opentabs.
Same thing with activeTabId and previewTabId. And then we have openFile, closeTab, closeAllTabs and finally setActiveTab. Great, so a very useful abstraction over our SushTan store. So what I want to do now is go inside of features and actually create a new folder called editor and I want to create store here and I want to create hooks here because what we've just done doesn't belong in the projects, right? So I'm going to move useEditor to our editor feature hooks.
I'm going to move it there. I'm going to update the imports, even though it doesn't matter, because I'm now going to move use editor store to our features editor store. I'm going to move it there and I'm going to update the imports. So let's see. Inside of our projects feature, we shouldn't have a store folder at all.
We can delete it. The hooks should only have these two. You can now close the projects folder but your new editor feature should now have use editor which calls use editor store from dot dot slash store use editor store and you should find use editor store right here and maybe the only thing that you have to fix is this import if you even have to. I don't think you do. I think it's exactly the same level as we had it in projects.
Great. So that's one thing I wanted us to do. So now before we can actually implement the logic so that when you click on a file it opens it. Well actually I think we can do it. Let me go ahead and do file1.ts and let's do file2.tsx.
So right now you know clicking on them doesn't really do much, so I think we can improve that now by going inside of Features, Projects, Components, File Explorer tree right here. And now what I want to do is I want to add the logic to, well, you know, open some files, right? So after we do these instances, like create folder and such things, I want to call useEditor. So not useEditorStore, specifically useEditor from featuresEditor hooks, useEditor, our abstraction. And then we're going to get all of those functions from our Csushtan store but for this project ID so we don't have to pass the project ID a million times So in here I can extract open file, close tab and active tab id.
I can now do all of those. So I'm going to start with open file. Let's go ahead down here. If item type is equal to file and let's go ahead and do const is active. If active tab ID is equal to item underscore ID Because we now have access to the active tab ID, right?
And let's scroll down here to the tree item wrapper. In the onClick add open file, pass in item ID, and pinned to be false, meaning this is just a preview. And then in the onDoubleClick() we do the exact same method, but with pinned set to true. OnDelete() let's go ahead and delete this to do one close the tab so if this tab happened to be open by at the time we are deleting it through the file Explorer let's close it right no need for it to be open great and I actually think let's see is there any other place where we could close tab so we just did it here let me go ahead down here Does it make any sense to do it here? No, because this is a folder I wrote to do close tab here, but this is a folder.
I don't think we should do that. I think the lid file is sufficient here. And I think that finally, oh, I'm not using is active anywhere. Okay, so that's one thing I have forgot. And again, only a file can be active because what does is active represent?
It represents whether we should open a file in an Explorer or not. There we go. IsActive is active. And I think that's it. Great.
So, should we see any differences? Well, I think we should. Let's go ahead and see. So when I click on this file, I don't think you can see, but there is a very subtle shadow, like a background, which kind of indicates it's open. So click on it and then click somewhere else.
You can see how it stays selected, right? Even if you kind of click in between preview and code, it stays selected. So this is the only thing we can actually see now because that's the only UI we have built. What we have to do now is we have to build the tabs, right? So let's go ahead and do that.
So in order to build the tabs, there's actually one more files convex function that we have to implement, and it's used for the breadcrumbs. Let me actually show you how it's gonna look like, right? So when I click on globe, for example, see this source components globe JSX or source index CSS, right? No matter how deep I go inside it, it can always find its way to its root file. In order to do that, we need to traverse up the parent chain.
So I'm gonna go inside of convex files.ts and we have a bunch of things here. So I will copy get folder contents because it's very similar. Let's copy it and let's just paste it here. Let me expand this as much as I can. So I'm going to go to this copied one and I will rename it to get file path.
And it will only accept one thing and one thing only. ID v.id files. So we start with the identity as usual. Then we go ahead and we get the file. If the file does not exist, we throw And then instead of arguments project ID, we use that file project ID to confirm, do we even have access to get the file path, right?
Let me explain actually what this is. In fact, I made a little comment. You don't have to copy the whole comment, but I think it will help you understand. So this function builds the full path to a file by traversing up the parent chain. The input is a simple file ID, for example, ID of button.tsx.
And the output will be an array of answer stores from root to file. So for example, if I have a folder source components button.tsx, I will get back an array with the first one being source, the second one being components, and the third one being my file. And then in the breadcrumbs, I can use it to display this. Is this the most optimized thing to do? It can probably be better.
Why am I saying that? Another solution of doing this would be to maintain the path, right? So I can have source, you know, components, button.tsx. This could be cool. But imagine if I want to rename the folder source.
What do I do in that case? I would have to, for one simple folder rename, I would potentially have to update thousands of files which all have this path cached and stored. So I guess the best solution is some kind of hybrid or some kind of background job that will take care of updating the path for all files. So in case you kind of shook your head and said, is this really the best thing we can do? Probably not, but at the current state of this project, I think it's more worth it doing this than modifying all of our deletes and update simple renames to be a super expensive query because this one isn't that expensive really, right?
And it won't really appear anywhere besides when you click and it will be cached on top of that. So I think it's an okay deal. Later you are welcome to of course modify to whatever you think is best. So I'm now going to remove everything here. I just want the checks, right?
And I'm going to start by defining constant path, which is gonna be an empty array, but in the array, each item will be an object which has underscore ID and name. And then let's go ahead and start with the current ID. The current ID is a type of ID files or undefined. And we start with arguments ID. While we have the current ID, let's go ahead and do const file open parentheses await context dot database dot get files passing the current ID as document files or undefined let's import document from generated data model here.
If there is no file, break the while. Otherwise, path.unshift, so add to the array The following object, file ID, name, file.name. And then the current ID simply becomes this file's parent ID until There is no current ID, meaning we found the root one, which has parent ID as undefined, meaning that will break the while method. So a pretty brute force way to do it, but it gets the job done. I've explained the complexity and the alternative moments before, right?
We could kind of maintain that, but the problem is a very expensive rename queries. So it's kind of a compromise. Again, you're welcome to improve this, of course, at the end of the project as a personal challenge. Awesome. So let's now go ahead and start building the UI.
So we have to go back to our source features, projects, components, And let's find the project ID view. We have added the file explorer but we never added the editor view. So now we're going to add the editor view. And it's going to have the exact same prop passed as the file explorer. So obviously we are getting errors and now let's go inside of editor, new file components and let's go ahead and do editor-view.tsx.
Now inside of this editor view let's export const editor view like this. Let's do a super simple type here, project ID, project ID, which is a type of ID, like so. And in here, let's return a div, class name, heightfull, flex, flex column, div. The div will have a class name, flex, items center, and finally top navigation component inside. And inside of here, we just pass in the project ID.
Great. So we now have top navigation. Before we start creating it let's import the existing editor view from features, editor components, editor view, but we didn't do much because now we have an error here of course. So I'm going to go inside of editor components, new file, and I'm going to call this top navigation.dsx. Let's export const top navigation and let's go ahead and return a scroll area from components UI scroll area and let's create a nav element.
This nav element will have a background color of sidebar. Flex items center height of 35 pixels or let's see height 8.75 8.75 border bottom and I believe that's it. And let's give this one a class name of flex one. And then in here I'm going to extract the project ID and just give it the exact same prop scenario as our previous ones. So let me go ahead and collapse this in case you can't understand what it is.
I feel like this might be easier. Now that we have the skeleton of top navigation, let's go inside of editor view. Let's import top navigation right here. And this should be project ID. There we go.
And we shouldn't see much difference besides this. There is now this black line here and you can see what it's going to become. It will become the tab, right? But right now just a tab, just a line should be rendered, right? Great.
We now have this. Now what I'm going to do is I'm going to get all of my open tabs for this project using my use editor hook and by passing in the project ID. And now that I have my open... Oh, my apologies, not here. So sorry.
Inside of the top navigation, there we go, here. Let's do it again. So inside of top navigation, I'm going to get all of the open tabs for this project and import use editor from hooks use editor, not the store one, the hooks one, the abstraction, right? And then inside of here I can do openTabs.map fileId and then index and I can return back a tab component And each tab component will have the following props. Key, which is a file ID, file ID, isFirst, which is a very simple index logic, and the project ID.
And let's also make sure to add inside of this scroll area scroll bar component from components UI scroll area and give it an orientation of horizontal. Great. Now it's time for us to develop the tab component. I'm gonna do that here. Const tab.
So first things first, what props will this accept? Well we defined them below but let's do it here. File ID which is an ID of files is first which is a Boolean and project ID which is an ID of projects. File ID is first and project ID. Great.
Now let's go ahead and just return something so we don't have any errors, great. And in here, first thing I'm going to do is I'm going to attempt to load the file. So use file. And I can see since I don't have autocomplete here we most likely didn't implement the hooks for useFiles. So let's quickly go inside of our projects, hooks, useFiles.
So I have a lot of them, but a lot of them are missing too. So let's see. Export const useFile accepts fileId to be an id of files or null and it will return useQueryAPIFiles and we have the function. Great. GetFile.
And if we have fileId, we pass that as the options otherwise we skip this query so now seems like that is causing a problem. Oh, it's file ID, not file. Okay. So that was use a file. And now we need to use our newly created one, File path.
So this is the new hook you need to add. Use file path which accepts the exact same thing and it returns use query API files get file path. If it has a file ID it will pass it along otherwise it's gonna skip this query. Confirm you have getFilePath and confirm you have getFile. I can see we implemented this in the 9th chapter file explorer so it should be here as well.
If it isn't, here's a very easy function for you to write. So it's actually the simplest of out of all of these. Okay, now that we have this, let's call useFile from features, projects, hooks, useFiles. So we just get the one and we pass in the file ID. Great.
Now let's call useEditor. So basically useEditor from hooks.useEditor. The one from our editor feature. Basically in the same folder here, right? I just want to make sure you're not accidentally importing this one.
Make sure you are importing that one. So use editor accepts project ID. And in return we get active tab ID, preview tab ID, set active tab, open file and close tab. All of those things. So now let's go ahead and add some constants here which are going to help us style this.
Is active if active tab id is equal to file id. Is preview if preview tab id is file id. File name did we load file.name if not loading. Now let's go ahead and let's give this div right here for the tab some attributes. On click, set active tab.
On double click, we just pin the file, which is already open. So I believe if let me try and find so open file open file open file again open file let's find it. This would be scenario 2. No, case 3. File is already open.
Just activate it and pin if double clicked. So that is this scenario here. When we open a file which is inside of our tabs it means it's opened but perhaps it's not pinned. And now in here we have to add a class name. So let's add a class name CNUtil.
Make sure you've added the import. Now, inside of here, let's start adding some classes. It's going to be some of them. So flex item center gap 2. Then we're going to add some height and some padding here.
So height 35 or what does it say? 8.75 PL and padding right functions. Then let's add cursor pointer text muted foreground Group and BorderY. Then let's go ahead and let's add BorderX and BorderTransparent and finally let's add on hover BorderAccent with a 30% opacity. Then for the first dynamic one will be if the tab is active.
So if it is active, render bg-background, text-foreground, border-x-border, border-bottom-background, minus margin-bottom by a single pixel and drop shadow. This will just create a kind of a cool effect. You're gonna see what it is. And let's just add is first. If it's first, border on the left side is transparent and put an exclamation point at the end which then results to important.
So inside of here if file is undefined let's go ahead and render a spinner from components UI spinner let's give it a class name text ring otherwise let's use the file icon from React Symbols. Make sure you didn't accidentally import one from Lucid React. So React Symbols, icons, utils. We're going to give this file name, file name, auto assign, and a class name size 4. Below that we're going to have a span rendering the file name with a class name, CN, text, small, white space, no wrap, and let's give it is preview italic.
I think this might be enough for us to start seeing something. So if I click on here, there we go. And you can see how they replace each other. But if I double click, You can see how it stopped being italic. And now when I click on another one, the preview is now handled in this other tab.
This one simply highlights back to that one. Right? Let's try creating file 3. You can see it only replaces what was previously in the preview tab. If I double click on file 3, that becomes permanent.
You can see how everything is synchronized. And clicking on file 2 opens a third preview tab. Great! Our logic is working and if you're wondering that like minus margin bottom pixel is doing this so it kind of looks like the border is going up and down So and this doesn't have a border. It's just like a little trick to make that cool effect.
All right. Now let's go ahead and implement the buttons which will help us close these tabs. So these will be normal HTML buttons. And let's go ahead and do on click here event prevent default and stop propagation and close the tab. Then let's go ahead and add on key down If the key is enter or if the key is an empty space, prevent default, stop propagation and close the tab.
And add a class name here, again, CNUtil. The first one will be just the general style, padding 0.5, rounded small hover background white with 10% opacity opacity 0 group hover opacity 100 and then for the second one if it's active opacity is just a constant 100 and an X icon from Lucid React. There we go. I think that is it. So now we should be able to close tabs.
Awesome! We have a fully working tab system here. Super cool. Now, let's go ahead and add the breadcrumbs, which are going to help us tell where this file is located at. To implement Breadcrumbs we have to go back, so let's go back instead of editor, components, editor, view, where we actually render the top navigation.
And now using this project ID we're going to actually use our previously imported hook, use editor. So import that from hooks use editor, get the active tab ID and Then outside of this div encapsulating the top navigation, we're going to check if we have active tab ID, let's render a file breadcrumbs. The file breadcrumbs will then again have a project ID as the prop. Now let's implement the file breadcrumbs. So I'm going to do that here again, file breadcrumbs.tsx.
Let's import, okay let's import react from React, file icon from React symbols. Let's import use file path from features, projects, hooks, use files. Once again, let's import use editor from features, editor, hooks, use editor. Let's import everything from breadcrumbs from components UI breadcrumbs. So item, page, link, list and separator.
Let's go ahead and import ID from convex generated data model. And Now let's go ahead and prepare the File Breadcrumbs component. The File Breadcrumbs component accepts a project ID, which is a type of ID projects. In here, two things. Extract ActiveTabID from UseEditorProjectID and get the file path from use file path.
This will allow us to get this in this kind of... Well I have it here right? In an array of ancestors from root to file and then we will be able to create this. Once we have that, let's check if filePab is undefined and there is no active tab ID. In that case, Let's just go ahead and return kind of a placeholder.
So a div with class name padding2, bg-background, padding-left4, and border-bottom. Breadcrumb, breadcrumb-list with class name on small, gap.5. Oh, it's always gap.5. Okay. Breadcrumb-item with class name text small, breadcrumb-page, and now this is a special Unicode sign for an empty string.
I'm not sure if this would do the same purpose. I think I didn't manage to get that result. That's why I'm specifically doing this in case you're wondering. So just a normal Red Crumb composition as a placeholder, right? This is a loading scenario.
And now we can kind of copy this actually. So let me just copy this and return it here. Because now we're going to actually be building the real thing. So close the div. Let's indent this back.
Inside of the breadcrumb list, let's get file path dot map, item and the index and let's go ahead and check, is it the last element So if index is equal to the overall length of the file path minus one because arrays started zero Let's go ahead and do react fragment here in this form. So react fragment give it a key item underscore ID, then inside we are going to render the breadcrumb item with class name text small. And now let's do two scenarios. The first one will check if is last and the other one will do well something else. If it is a last we're going to render breadcrumb page with class name flex items center and gap one inside of that breadcrumb page we're going to render the file icon with file name auto assign and class name and next to it the file name and alternatively We're just going to render the item name using a breadcrumb link.
Nothing more. I will collapse it so it's easier to look at. Like this. Okay. And then Let's go outside of the breadcrumb item and do if it is not last render breadcrumb separator.
Like that. There we go. That's it. Now that we have the file breadcrumbs we can now go back to the editor view and we can import file breadcrumbs. Let's check it out.
So now when I go ahead and I open a file, whoops looks like something's not working. Could not find public function for files get file path. Oh, that's because I don't have it running. Npx convex dev. This will synchronize that new function or it will show us an error if something's wrong.
Let's go ahead and try this again. I'm gonna go ahead and open a file. And you can see right now it's super simple. Right? Because these are root files.
But if I go ahead and if I create a new file instead of here. So nested file. And if I click on it, you can see how it tells me exactly what folder it's in. And if I go ahead and create a folder 2 and if I go ahead here and create a folder 3 and inside folder 3 I create supernested.tsx and click on it. You can see that it can traverse up the path until it finds the exact child.
One noticeable difference I can immediately tell from the original one is that the gap between the separator and items is much smaller. So I just want to quickly investigate why is that. So I'm going to go back instead of file breadth crumps here and I think I found exactly why. Let me see. I think I know.
Remember how I removed this small gap 0.5 because I thought logically it's not needed but looks like you have to explicitly tell it that even on small make the gap smaller. There we go you can see that now it looks better. Currently clicking on it doesn't do anything. Later you could implement something like a drop down of the current files, but for our use case this is more than enough. So yes, it has a brief loading state, but after that it actually hits cache every time.
I'm pretty satisfied with how this turned out. Amazing! Now let's actually implement the editor down here. We are not going to implement any AI features or things like this, we're just going to make sure we have the basic CodeMirror6 added in this project. So in order to implement the code editor down here, we have to start by defining what happens when a file is not selected.
So that's actually quite an easy state. The only thing we have to do first is head to Polaris assets and in here find logo alternative. I found it from the exact same place where I found logo SVG. So logo Ipsum and untitled UI. So now let's go ahead and add this.
I just copied the raw file. I'm going to go inside of public and I will add logo alt SVG. I'm going to open this file and just paste the code inside. If you really want to, you can use the exact same icon which is your main logo. And then let's go back inside of the editor view.
And now let's go ahead and do a scenario which is if there is no active file. So let's also do const active file to be used file from features projects hooks use file and pass in the active tab ID like this. So that's our active file. And Now we're going to check within a div which we give a class name flex1 minimum height of 0 bg background. If there is no active file whatsoever or we maybe can't load it.
Let's go ahead and create a div with a class name size full flex items center justify center and inside we import image from next image. So make sure you add this and we give it source logo. My apologies. Logo dash alt SVG out Polaris or whatever is the name of your project width of 50 height of 50 and the class name Opacity 25. Let's take a look at that.
So now if I close this you can see that I have like a nice placeholder which is branded as my project. Until I click on a file and it loads you can see that then it's supposed to load something but right now it isn't loading anything. So what we're gonna do is do the opposite. If we have an active file, in that case, let's go ahead and render code editor, which we don't have. But let's go ahead and just...
Well actually, let's not pass any values right now we're going to implement values later so now let's go inside of editor components let's create a new file code editor.tsx and let's see how do we set it up. So we are going to be using CodeMirror specifically CodeMirror version 6 and this is kind of a basic example. So let's see. Examples, basic editor. Looks like this is enough.
:10 So I'm going to go ahead and do the following. I will install. We're going to do all of these later and dive in like what are all of these classes. But let's start with I don't know maybe something like this. Npm install code mirror and let's do npm install code mirror forward slash language dash javascript.
:37 Let me show you what versions I'm working with. I think minor versions are not as important as the major one, which is version 6. So you can see my code mirror is 6.0.2 and my lang javascript is 6.2.4. Basically both are on version 6. Now that I have that, I'm going to go ahead and export const code editor and in order to kind of render this I'm gonna go ahead and do const editor ref use ref from react html div element and pass in null.
:21 Then const view ref, use ref let's pass in editor view or null and null here as well. Now let's go ahead and return div ref editor ref with a class name sizefull pl4bg background and it's actually a self-closing tag there's nothing we need to pass inside and it looks like this is an unfortunate name here. It's called editor view. And we should import it from a package code mirror forward slash view. So let's actually install that package.
:18 Npm install code mirror forward slash view. And then I'm gonna show you my package. Jason again, there we go. 6.39.8 in case you wanna use the same version of my code mirror forward slash view. Why do I say unfortunate name?
:35 Well, because we have editor view right here. So be careful not to import this one. So let's manually go here, editor view, not there we go, this is a mistake, not that one From CodeMirror View, like that and maybe I can import Type Editor View, so it's very strictly used as a type, I don't know Ok, so that's the ViewRef and I think actually we will use it later. And for now let's just call a use effect which we can import from React. And inside of here I'm just gonna go ahead and create const view, new editor view.
:23 Document will be start document. Parent is going to be editor ref.current. Like that. Editor view can't be used as this. Okay, so remove type from here.
:39 Okay, before we do it, let's actually check if there is no editor ref dot current return. So we get rid of that error and then in here extensions are going to be an array the first one will be basic setup and we should be able to import basic setup let's see from code mirror itself okay from code mirror let's see And it looks like it also imports EditorView. So yeah, you can import that from here too. So basic setup. And let's import JavaScript and pass in TypeScript to be true.
:25 And where do we import that from? Well from the package we installed previously, CodeMirror, Blank, JavaScript. Just destructure the import like this. Okay, so the ViewRef I think will not be used. Maybe it will.
:43 Let's see. So Now down here after we initialize the view, let's actually do view ref dot current and pass in the view and let's go ahead and create an unmount function, view dot destroy. Oops. There we go. And I think that this should be enough for us to just render a super basic code editor here.
:11 Let's go back instead of the component editor view and import it from dot slash code editor. And now when I go ahead and select the random file, I should see a text start document and I should be able to type in here. And you can even see that there is some kind of syntax obviously doesn't work well because this is a light themed syntax put into our dark mode app so we are going to have to change the theme but try the indentation on or things like that try collapsing you can see that it works right There are a few bugs here and there, like this indentation is kind of being funky right now, but we will work towards fixing all of those things right now. So what I want to do now is I want the ability to, well, it makes no sense to load the actual like value now because all of these files are empty. So all of them are going to be completely empty.
:16 It makes no sense to load that. Instead, how about we go inside of Code Editor and we change the document to be just like a super simple implementation. Let me go ahead and see, can I write this like const, I don't know, counter? And then in here, const value set value, use state zero const on increase call the set value value value plus one like something super simple on decrease and return and in here a div maybe div and in here a button which shows the current value duplicate that I don't know I'm just kind of making things up as I go on, increase and on click on decrease. Actually how about we just do one.
:33 There we go That's kind of code that makes sense. So some default value. That's kind of a good example of a JSX document, right? So that we can work with this. Currently looks horrible.
:47 So we are going to work our way through improving it and adding it some fun things, right? So let's see, what is the easiest thing we can add here? Well, I think the easiest thing we can add is the one theme, one dark theme. It's called like that, right? So let me go ahead and expand this and let's do npm install code mirror forward slash theme one dark.
:17 Let's install it. I'm going to show you my package. Jason CodeMirror theme one dark six point one point three. You can see that six is kind of the only important thing here. Once we have one dark theme, I'm going to go ahead and import one dark.
:38 And all I'm going to do is I'm going to add it to my extensions list like this, one dark. And I'm going to save. And then I'm going to have to refresh here. And let's see, hopefully it kind of looks better now looks much much better how about this try copying your entire file from your actual editor and paste it here look at that looks pretty good you can't scroll, but it looks pretty good. You can fold things, right?
:10 Looking really, really good. But not perfect. So, we are now going to add more and more stuff here until it looks better and better. So how about we start by giving it a full height team. This should enable us to scroll down.
:30 I think that one is an easy win too. So, the place I want to develop that is within the editor, but I'm going to specifically start calling these extensions. So they aren't combined with components or anything like that. And I will call this one theme.ts and let's do export const custom theme editor view from code mirror not from our components right and let's go ahead and call dot theme here The first one will be and which simply hides the outline. Then we're going to go ahead and add cm content here and the font family.
:16 I think this is actually not the correct variable. Where do we find the correct variable? In our source app layout. So we added font inter and fontplex mono. Make sure yours is the same set of source app layout.
:31 You can see it's just simple next font Google imports. Copy the variable name and now add it here. And also double check in your globals.css. So your source app globals.css all the way up here. Font Mono should also be font Plex Mono here.
:54 Great. So that's CM content and font size 14 pixels. CM scrollers scroll bar with thin and transparent. And let me see. Let me see.
:08 I think I'm I'm missing something because I have a feeling like none of these will actually help me with my scroll situation. Let me refresh. Oh, I didn't add this theme so I won't see it anyway. So I have to go inside of components, code editor right here. And we have to add the theme.
:34 So after one dark I'm going to add custom theme. Which we import from extensions theme. I'm going to refresh again. Let me open this. And I don't think much has improved.
:54 I mean nothing that I can see and I'm still having a problem with scrolling. So I still cannot scroll. I'm trying to figure out why that is. I mean this happened to me as well. I just can't pinpoint what exactly was the fix.
:10 I was kind of sure it was going to be the theme here but Maybe it is not. So let me go ahead and kind of discover a bit. All right. So I did some research and actually couldn't don't really understand why we are having the issue that we're having. You know basically you can see it doesn't take a hundred percent of the screen.
:33 Right. So if I copy along I mean this is how I tried debugging. So instead of my code editor I changed from BG background to read 500 and I saw OK so it's taking a hundred percent of the space, right? So why is the editor ref not being populated a hundred percent? So then I went inside of my extensions, custom theme here, and in this one I gave it a height of 100% and that actually fixes it.
:05 So if I go ahead now inside of this you can see there's space down here and if I go inside of code editor and I copy something along, you can see that now I can scroll. But I'm not sure why. Because this was not needed in my original source code. So I'm just not 100% sure why and I'm not sure how it's going to behave going forward. Let me try with zoom in and zoom out.
:37 It seems to be working very well. It's important that no lines are being cut off. Let me try triggering search functionality. You can do command F and that should trigger the search functionality. So let me try closing it.
:54 That doesn't seem to create any problem either. So I'm very confused right now. I have no idea why in my original source code I didn't need to put height 100 but here I need to put height 100. So what I'm gonna do now is I'm just going to make sure that I'm using everything the same as in my original source code. For example, I don't import from CodeMirror here.
:20 Instead, I import from CodeMirror a view. So I'm just going to make sure, okay, I'm going to use that here too. Let me try. If I comment out height, Does that maybe resolve it? I doubt it.
:32 It shouldn't. Makes no sense for that one because they're essentially the exact same thing. Oh wait. Was it that? No it's not.
:41 Because I can see the scroll bar is here. Which means if I copy something larger, yeah I can't scroll. No matter what I do. Okay, So looks like we just need height 100 in our custom theme. I'm not sure why.
:57 So if it's not working for you. OK. You know just continue going through this chapter and I'm going to investigate this later so we can determine exactly what's happening here. How come I didn't need this in my original source code but I need it now? Maybe the answer is in some other components.
:14 I don't know but I double checked everything and everything looks fine. So, yeah. Let's just, you know, wing it and we're gonna use our custom theme like this. Okay. And now I think that we kind of hit the limit of what we can do ourselves.
:34 Well, maybe the only thing we can improve is implementing our own language extension recognizer. Because right now this only works for JSX or TS files. If I, for example, try creating, I don't know, a index.html you can see it's using the invalid syntax. Well unfortunately this will work too but maybe not fully. Try some other language inside.
:09 I think we can maybe copy something here. Let's see. Do we have anything here? Like a readme? Try pasting it in here.
:19 Yeah, you can see this is not correct. See the syntax breaking, right? So how do you implement a smart recognizer of syntax? Well, step by step and one by one. So what we're going to need to do is create another extension here.
:36 So inside of your extensions, create a new file. I'm going to call this actually, Okay, yeah, let's go here. And I will call this language extension.ts.ts specifically. And now here's the thing, you decide how many languages you want to support. So I imported CodeMirror State, CodeMirror Lang JavaScript and now what I have to do is I have to install all of these packages.
:11 So pause the screen and install all of them and I'm going to show you my versions. So these are all the packages, CodeMirror, HTML, CSS, JSON, Markdown, Python. And there's so many more you can do but you have to install them all and you have to configure them. So now While this is doing, let's go ahead and let's export const getLanguageExtension, acceptFilename, which is a string, and return extension from CodeMirrorState. Now In order to get the extension from the file name, we will split it by a dot separator, get the last one and lowercase it.
:56 And then we're going to use a switch case on the extension. If case is JS, we are going to return JavaScript, right? And so on and so on. So I'm just going to go ahead and copy and paste the rest. Here it is.
:16 So let me show you so you can pause. JS, JavaScript, JSX, JavaScript with the config. Same for TypeScript and TSX which enables both of them. HTML, CSS, JSON. For both MD and MDX we use Markdown.
:31 Python and by default an empty array. Great! So that's the language extension. I didn't show you my package json so here it is if you want to see all the languages that I have. Again six, the major version is the one that's important.
:47 Now that we have the language extension, let's go ahead and initialize it here. Const language extension will be useMemo and Let's go ahead and call it like this, get language extension. From extensions, language extension. And pass in the file name. The problem is we don't have the file name, yes.
:14 So okay. Pass in the file name and put it in the dependency array, file name. So let's import useMemo from React and for the file name let's go ahead and create an interface, props, file name, string, extract file name and assign the props. And now we have that error resolved. And now Inside of here, after you do the basic setup, let's add language extension in place of the JavaScript one.
:57 Like that. Let me go ahead and maybe write this in a prettier way like this. Remove the unused import now. Now let's go back to the editor view where we actually do this and let's pass in file name to be active file.name like that. There we go.
:22 So now when I open TSX it should oh I have to refresh first yes always refresh first because the hot reload does not affect this. So you can see this is now TSX, right? But you can see how here the syntax breaks, which is correct because this is an HTML file, right? So it shouldn't render the same syntax. And just like that, we've implemented our own language syntax decider.
:52 And now let's see what else can we do. So for example our tab indentation currently sucks. It doesn't work. Let's go ahead and improve it by adding CodeMirror commands. So npm install, let me go ahead and fix this, CodeMirror commands.
:16 And once you install it, let's go ahead right here, import indent with tab from code mirror commands. And then down here, after language extension, add key map dot off. And inside of here add indent with tab. And the keymap solution comes from the following. So I'm going to do a slight modification here simply so I have the exact same as my source code because I'm still bazzled you know why it works on one example but doesn't with the other.
:54 So edit review from code mirror view and key map from here. And now you should be able to indent with tab. Let's go ahead and see. Again let's do a refresh. Let's go ahead and try.
:15 There we go. You can see that now it doesn't escape the editor. I can now safely indent. Who knows, maybe it was some of these extensions which enabled the full height view. Right, but let me try pasting again.
:30 It seems fine. It seems to work okay. Right? I'm not sure what was the issue. But I'm just confused.
:36 Why do I need it here? But I didn't need it in my source code. But still, seems to work great. Okay. What else should we do?
:44 How about a minimap? Right? You see this? How about we add that? So that is actually implemented by Repl.it, right?
:59 They actually have a package of their own. So basically a bunch of community packages exist for CodeMirror and this one is from Repl.it because Repl.it itself decided to use CodeMirror over all the other editors that exist. So I actually gave you a very good and tested solution. Once we install that, let me show you my package. Let me show you my package.
:21 So, replit-codemirror-minimap 0.5.2 0.5.2 if you want to use the same version. And we're going to head inside of the extensions here. And I will just add minimap.ts And I will just add minimap.ts I'm going to import show minimap from replit code mirror minimap. I'm going to do a simple create minimap And the last time I mentioned minimap, hopefully, export const minimap. With show minimap, compute, document and return create and the create method above.
:58 Alright now that we have that we can go back inside of the code editor and after keymap off, add minimap from the extension. Right here, extensions, minimap. There we go. Let's try it out. So copy code editor entirely.
:20 Make sure to refresh this page. And let's go ahead and select the file, paste things inside and look at it go. A beautiful mini-map right here. How about we add another useful feature? For example, you can see in my finished source code, I have this kind of indentation indicators.
:43 I can see the depth of indentation. That is actually also handled by replit. So let's go ahead and do a quick install. NPM install replit code mirror indentation markers and I'm going to show you What package.json version I'm using so package Oops here this package.json 6.5.3 version for the indentation markers This one is much simpler to add. We just import indentation markers from Repl.it CodeMirror indentation markers.
:26 And let's go ahead now and just add them after the minimap. Like that indentation markers. And I believe that that is kind of the end of what we can do without modifying the basic setup. Here it is, You can see how we now have indentation markers here and how they change depending on the indentation level. So what do I mean by the end of what we can do without modifying this basic setup?
:03 Well, here's the problem. You see this kind of fold gutters? You see this? It kind of looks weird. It's not centered.
:14 And if you try to use CSS to center it, you will very closely come to a limitation, very soon come to a limitation because this is just a Unicode, this is a character, it's not actually an SVG icon. You can see how good it looks on my finished source code here. And the problem is the fold gutters do exist as something you can extend, right? So you would have to add fold gutters here and then you would do render icon or whatever is the syntax, SVG, blah, blah, blah, you would add your own. The problem is fold gutters are already implemented in here, in the basic setup.
:55 So that's a problem. You can't implement it twice and you can't extend basic setup. But thankfully, the creators of CodeMirror have that solution for us because Basic Setup is an amazing set of all the packages you need to have your app up and running. So using the link on the screen, you can... First, I'm going to show you how to find it yourself, but also, if you cannot find it, you can always find this, my assets folder.
:21 In here, we have the custom setup file entirely everything we're going to need why am I telling you to copy this and why are we not implementing this well because I didn't implement it either All I did was I searched for CodeMirror basic setup on Google and this repository came up from CodeMirror basic setup and inside of the source I have CodeMirror.ts and here it is. The entire thing, right? And here it is, the fold gutter, the one I want to modify the icon for. So that's how you do it. And it's actually intended to be used like that.
:58 So the extension itself does not allow customization. The idea is that once you decide you want to configure your editor more precisely, you take this package's source code, which we are doing, and copy it into your own code. That's exactly what we are doing right now. So we are doing this in the way it was intended. So using the link on the screen, you can find Polaris assets and in here custom setup or you can just, you know, visit the source code and you can just copy the entire thing here.
:24 So it is exactly the same as this one here. The only difference is I added SVGs for folder gutter icons and everything else is I'm pretty sure the same. Maybe I did some modifications, but I highly doubt. And I kind of removed the comments and everything else to make it cleaner. So I'm going to copy this entire thing.
:46 And then I'm going to go inside of my extensions again. So in here in the extensions, new file custom-setup.dsx, my apologies, .ds and let's paste the entire thing inside. That's it. I'm not even going to go through this because you know it's just a copy from the source code. So the only thing we do is we modify the folder gutter because I wanted to have a nicer SVG icon than whatever it's currently using.
:16 And once I have custom setup I can go back inside of the code editor and I can remove basic setup in place of custom setup. From extensions custom setup and let's remove basic setup from CodeMirror now. Alright let's go ahead and check it out now. And now we should have a much nicer icon to collapse things. There we go.
:41 This looks much, much better. Perfect. And see this happening? You can kind of select this. This is the reason why I went inside of my global dot CSS and down here I added it to do add select none later So that's why I did that select dash none We can bring it back now because it actually just works better You can You shouldn't be able to select things anywhere here.
:10 Everything kind of is supposed to look like and feel like an app. So that's why I did that. Alright! Awesome! So there are definitely more things we can do with our code editor.
:25 But I think I want to keep that for another chapter simply because those are AI related things. So it makes no sense to implement them right now because we won't be able to implement the entire thing. But one thing I want to do is the ability to preserve content. Right. So I want to give you the option to save a file.
:47 Let's start by changing the code editor interface. So besides file name, let's also give it an on change and let's also give it an initial value. So this is not going to be a controlled component because if it is a controlled component, if you constantly accept the new updated value, it's very annoying because you are typing something here and it receives a new like updated value of this document and it will reset your cursor up here. So it's super annoying. So because of that we are not going to treat it as a controlled component.
:25 We're just going to accept the initial value once we load and reset the initial value on every file change. All right, so we have editor view, view ref, we have language extension here. Let's change this document to instead be initial value. And yes, we can destructure these now. So file name, initial value and on change.
:49 All right so we are using initial value here document initial value perfect. Let's see what are we missing here we are missing language extension here And we actually don't need the initial value because it's only used for the initial document. So we don't want to track its changes. So this isn't the best thing, but let's do SLint disable next line react hooks exhaustive depths and then I'm going to add a little explanation so dash dash initial value is only used for the initial document I will see if there's like a prettier way of doing this but for now it gets the job done so we don't have to pass the initial value here. Great.
:39 That part is now done. And now let's go ahead and implement the onChange. I first just want to confirm that we have the necessary files here, too many files open, inside of convex files. Do we have update file? We do.
:59 Great. Now let's go ahead inside of source, features, projects, hooks, use files. And the same thing as we have use create file, let's go ahead and do use update file. And let's just call update file. That's it.
:19 And we need to add... I'm going to add to do add optimistic mutation to these things simply because they make the app fill that much faster, right? But we're not going to do it now. We don't need to do it for queries and we don't need to do it for this because it makes no sense, right? Because this is not a controlled component.
:42 So now that we have this, let's go inside of the editor view.tsx right here. So the component right. And what I'm going to do is I'm going to add const update file use update file A hook from features projects hooks use files. Let me reorder my imports just a little bit here. Okay.
:15 And now that I have that, I'm gonna go ahead and define a timeout ref because I want to create a debounce. So I don't want to update on every keystroke even though convex can probably handle that still. No reason to do so. Let's be gentle with our updates. So use ref from react and we're going to implement our very own the bounds here.
:41 So let's go ahead to the code editor right here. First things first it should reset on every active file ID, right? And then let's give it the initial value active active file dot content or an empty string file name is already given so onChange should accept the content which is a type of string first things first if we have an existing debounce timeout clear it otherwise let's go ahead and let's timeout so we're creating a timeout for this much so we could store that up here const debounce in milliseconds like that so you can easily change it later without having to find the exact function there we go, like this it will update using id active file and the new content Now we have to go back inside of the code editor here and actually use onChange. So after indentation markers, use editorViewUpdateListenerOf update if updateDocChanged onChange use editor view update listener of update if update doc changed on change actually it's going to be required so you can pass in on change update state document to string. And let's see on change.
:24 It shouldn't be optional. It's required like that. And I think That should work. So let's try. Let's copy code editor here.
:35 And let's see. Last saved 15 minutes ago. I'm gonna refresh this entire project now. I'm gonna go here, select file 1, still. Saved 25 minutes ago.
:46 And I'm gonna paste the entire code and I'm going to wait for a second. So now saved less than a minute ago and if I refresh and if I click on file one, there we go. We are successfully persisting our content. Amazing. Auto save.
:05 No need to save. Let's now see does it actually do something. Right. I'm going again waiting. I'm waiting.
:15 I'm waiting. So updated definitely. And it didn't move my cursor, at least not what I've noticed. So, that's what I was testing, right? I think it works very, very well.
:27 Let's see how it behaves when I change different files. Works just as well. Amazing. I'm super satisfied with this. We still have one mystery and that is why do I need this?
:40 I just want to test one more time. Is it maybe once I add a bunch of these other extensions, that something magical happens here? Oh, it looks like it is. Yeah, when I add a bunch of extensions, maybe it's like the minimap or something. You can see that at one point we no longer need height 100.
:01 I'm not sure why. Maybe it's the minimap? Maybe it's the indentation markers? Let me try commenting this out. Oh yeah, that's what makes it able to scroll.
:14 Okay. So yes, when we have minimap and indentation markers on, probably just one of them, the scroll is 100%. So if you have those two, you can most likely remove height 100. So however you wanna proceed, I don't think this will hurt. So maybe we can keep it here, but looks like those extensions of ours do the job as well Amazing so I'm just trying to see if there's Any last thing we can do, but I think you've worked hard enough.
:49 There is just one thing actually, yes, I'm sorry. And that is we shouldn't always render the code editor. So let's go ahead and do the following. Const isActiveBinary. Is active file a binary file?
:06 So if we have an active file and if active file has a storage id it means it is. Is active file text? If we have an active file and if not active file dot storage ID. Don't make the mistake of checking if we have active file dot content because we don't have to. A file is allowed to be empty.
:31 The question is if we have storage id it means this is not supposed to have any content at all. And now let's only render the active file and the code editor if is active file a textual file. And this should still work just fine. You know what, maybe instead of doing this, we can go inside of CodeEditor and change the initial value to an empty string. Because we obviously want our files to be empty in the beginning so why don't we modify the code editor to accept that rather than this and then we can make the initial value optional.
:15 There we go. That looks much cleaner on our side. Perfect. And then let's go ahead and do if isActiveFileBinary and we're just going to do a paragraph to do implementBinaryPreview. Great.
:34 So, let's just do one more check and then we're going to open a pull request and oh yeah this is super annoying. Can we please go inside of file explorer index.tsx and just change the default value of isOpen to true. It's super annoying that we have to open it every single time. So now when I refresh, it's opened by default and everything here is saved. Amazing, amazing job.
:11 Works super well. We have everything here. Awesome. So let's go ahead and let's merge this, right? So we have 18 files, 19 files, right?
:23 Package, JSON package, lock files, new image, globals, code editor, editor view, breadcrumbs, navigation, custom setup, language extension, minimap, theme, use editor, use editor store, project ID view, index.tsx tree, and use files. Okay, let's go ahead and stop all of our terminals here. Chapter 10, so git add dot git commit chapter 10 code editor and state. Great git checkout dash b 10 code editor and state like this, So code editor state and git push uorigin10 code editor state. This will then push it.
:13 You should be able to see the new branch down here. And let's go ahead onto our github. Let's open a pull request and let's let CodeRabbit review this massive PR we've prepared. And here we have the summary. We added code editor with syntax highlighting, minimap, indentation markers, and multi-language support.
:39 We added file breadcrumb navigation showing a file hierarchy, editor tabs for managing open files with preview and pinned modes. We integrated File Explorer for direct file opening and tab management. And we disabled text selection across the application. So it looks like we were pretty good. Only three actionable comments.
:02 We do have some nitpick comments but these aren't critical right so you can always see the nitpick comments right. They personally define them as that and they hide it by default. Right for example potential stale closure on change not in effect dependencies. So yeah, technically that is correct, but you can see it doesn't actually break anything. So in here it's actually warning us about adding this select none thing to our body.
:35 So it's concern is that it breaks the editor itself that you can select within the editor. I can personally still select. I'm not sure about other browsers or if you have bad experience with SelectNone, feel free to remove it from the body. You don't need it. I just think it's like a cool thing to make it look like a native app but yeah, maybe not the smartest solution given all the browsers and mobile ones and who knows what select none will actually do so perhaps in my next chapter I might remove it and just revert it to as it was so good comment here inside of here we actually do have a real mistake we never clean up pending debounced updates.
:21 We clean it up here, but we don't have a use effect with unmount to make sure the timeout ref is cleared. So this can cause memory leaks from uncancelled timers, attempted updates after unmounts, and updates to the wrong file if active file changes before the timeout fires. So a very serious issue that we have to fix in the next chapter. Thank you CodeRabbit. In here it doesn't like the fact that we removed the outline but it just looks so much better without it so I'm going to keep it removed in this case.
:01 Awesome! Let's go ahead and merge this pull request. We just did an amazing job. So almost a thousand new lines. So we have chapter 10 right here.
:12 Let's go ahead back to main, git pull origin main. We are now synchronized completely. I like to do a little sanity check. I should be on main branch and inside of my graph I should see 10 detached and then merged back here. Amazing!
:35 So I believe that marks the end of this chapter. We implemented Shushtand state management, CodeMirror 6, OneDark theme, tapped file switcher, syntax highlighting and a bunch of other things actually. Amazing, amazing job and see you in the next chapter.