I cloned a simple VScode using Tauri and ReactJS
Tags: react, tauri, tailwindcss, rust
Published: 2022-11-04 10:15:39

Hi friends, it's hudy again

Recently, i wrote a tutorial about how to build a notebook - desktop application - using Tauri. But, maybe it's not showing up the power of Tauri. Due to that i  decided to build another app - a simple code editor (a very basic version like VScode)

This app has some basic functions such as:
- reading files and folders that contains in a project folder
- creating new file
- editing file content
- display opened files in tab
- showing folder structure
- file icons

## Prerequisite
- [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites)
- Reactjs
- Understanding how React Context works

Final result:

![final-result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/353xybqfl713c25cu8az.png)

Source code: [https://github.com/hudy9x/huditor](https://github.com/hudy9x/huditor)

Download link (only for window): https://github.com/hudy9x/huditor/releases/download/v0.0.0/huditor_0.0.0_x64_en-US.msi

If you're using MacOS, please clone the repo and run build command as follow
```shell
// make sure that you installed Tauri already
$ yarn tauri build
```

Here is the youtube video tutorial version: 

{% embed https://www.youtube.com/embed/LUcUn-_KVXo %}

## Code structure
```shell
src/
├─ assets/       // file icons
├─ components/   // react components
├─ context/      // data management
├─ helpers/      // file system api & hook
├─ types/        // file object store
├─ stores/       // typesrcipt types
src-tauri/       // tauri core
```
And belows are main packages that we're gonna use in this tutorial
- [codemirror](https://www.npmjs.com/package/codemirror)  -  a code editor component for the web
- [remixicon](https://www.npmjs.com/package/remixicon)  -  simply delightful icon system, but only use several ones 🤣
- [tailwindcss](https://www.npmjs.com/package/tailwindcss)  -  a utility-first CSS framework
- [nanoid](https://www.npmjs.com/package/nanoid)  -  unique string ID generator
- [cm6-theme-material-dark](https://www.npmjs.com/package/cm6-theme-material-dark)  -  the basic dark theme


## Code flow explanation

Before jumping into the code, you should take a look at the overview of how the code editor works. See the image below. You'll see that our code editor has 4 main parts: Titlebar, Sidebar, Tab and Editor

![Main code parts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fba09qdkvnfkbdfk3t80.png)

Each parts are equivalent to one specific reactjs component. For instance: Tab will be `<Tab/>` component, Titlebar will be `<Titlebar/>` component and so on

Next image illustrates how our code runs after user load a project folder or click on a file/folders

![Code flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t284ittr29vwgg7ls81r.png)

Initially, user load project folder on (1) Sidebar. All files/folders will be saved to (3) Stores - only metadata saved, not file content. 

Next, every time an user click on a file then file's id will be passed to (2) SourceContext - our state management. 

When (2) get a new `selected_file` then automatically (4) will add a new tab with file's name, and (5) display selected file's content. End

## The core of our editor - the most important files

In order to read folder's content (Ex: files, folders) and get file content then the following files are core:

1. helpers/filesys.ts - contains functions that calls tauri commands for reading folder, getting file content, ... from `main.rs`
2. src-tauri/src/main.rs - defines tauri commands, and call functions from `fc.rs`
3. src-tauri/src/fc.rs - contains main functions to reading folder, getting file content, ...
4. stores/file.ts - stores file metadata

Ok, that's enough, let's get right into it

## Coding time

Start development mode
```shell
$ yarn tauri dev
```

### 1. Scaffolding code base

First step, initialize the project code base. Run the following commands and remember to select a package manager - mine is `yarn`
```shell
$ npm create tauri-app huditor
```
Install somes packages
```shell
$ yarn add remixicon nanoid codemirror cm6-theme-material-dark
$ yarn add @codemirror/lang-css @codemirror/lang-html @codemirror/lang-javascript @codemirror/lang-json @codemirror/lang-markdown @codemirror/lang-rust
```
In this tutorial, i'm gonna use tailwindcss for styling
```shell
$ yarn add tailwindcss postcss autoprefixer
```
After installing, head to this official [guide](https://tailwindcss.com/docs/guides/vite) for completing the installation 

For saving time, i created a style file here: [https://github.com/hudy9x/huditor/blob/main/src/style.css](https://github.com/hudy9x/huditor/blob/main/src/style.css)

### 2. Customize the Titlebar

OK, The first thing we're gonna build is Titlebar. It is the easiest part in the tutorial. Because it just has 3 main features and we can use `appWindow` inside `@tauri-apps/api/window` package to do these

```javascript
import { useState } from "react";
import { appWindow } from "@tauri-apps/api/window";

export default function Titlebar() {
  const [isScaleup, setScaleup] = useState(false);
  // .minimize() - to minimize the window
  const onMinimize = () => appWindow.minimize();
  const onScaleup = () => {
    // .toggleMaximize() - to swap the window between maximize and minimum
    appWindow.toggleMaximize();
    setScaleup(true);
  }

  const onScaledown = () => {
    appWindow.toggleMaximize();
    setScaleup(false);
  }

// .close() - to close the window
  const onClose = () => appWindow.close();

  return <div id="titlebar" data-tauri-drag-region>
    <div className="flex items-center gap-1 5 pl-2">
      <img src="/tauri.svg" style={{ width: 10 }} alt="" />
      <span className="text-xs uppercase">huditor</span>
    </div>

    <div className="titlebar-actions">
      <i className="titlebar-icon ri-subtract-line" onClick={onMinimize}></i>

      {isScaleup ? <i className="titlebar-icon ri-file-copy-line" onClick={onScaledown}></i> : <i onClick={onScaleup} className="titlebar-icon ri-stop-line"></i>}

      <i id="ttb-close" className="titlebar-icon ri-close-fill" onClick={onClose}></i>
    </div>
  </div>
}

```

### 3. Create the SourceContext

As i mentioned above, in order to components communicate with each others we need a state manager - `<SourceContext/>`. Just define `IFile` interface fisrt

```javascript
// src/types/file.ts

export interface IFile {
  id: string;
  name: string;
  kind: 'file' | 'directory';
  path: string;  // d://path/to/file
}
```
Create a file called `src/context/SourceContext.tsx`. It contains 2 main properties are: 

1. `selected` - stores the file id the user click on

2. `opened` - contains a list of file id. Ex: ['387skwje', 'ids234oijs', '92kjsdoi4', ...]

```javascript
// src/context/SourceContext.tsx

import { createContext, useContext, useState, useCallback } from "react"

interface ISourceContext {
  selected: string;
  setSelect: (id: string) => void;
  opened: string[];
  addOpenedFile: (id: string) => void;
  delOpenedFile: (id: string) => void;
}

const SourceContext = createContext<ISourceContext>({
  selected: '',
  setSelect: (id) => { },
  opened: [],
  addOpenedFile: (id) => { },
  delOpenedFile: (id) => { }
});
```
Create `<SourceProvider/>` to passing our states to child components

```javascript
// src/context/SourceContext.tsx
// ....
export const SourceProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
  const [selected, setSelected] = useState('');
  const [opened, updateOpenedFiles] = useState<string[]>([]);

  const setSelect = (id: string) => {
    setSelected(id)
  }

  const addOpenedFile = useCallback((id: string) => {
    if (opened.includes(id)) return;
    updateOpenedFiles(prevOpen => ([...prevOpen, id]))
  }, [opened])

  const delOpenedFile = useCallback((id: string) => {
    updateOpenedFiles(prevOpen => prevOpen.filter(opened => opened !== id))
  }, [opened])

  return <SourceContext.Provider value={{
    selected,
    setSelect,
    opened,
    addOpenedFile,
    delOpenedFile
  }}>
    {children}
  </SourceContext.Provider>
}
```

In addition, i also create a hook called `useSource` that helps using these above state and function more easier


```javascript
export const useSource = () => {
  const { selected, setSelect, opened, addOpenedFile, delOpenedFile } = useContext(SourceContext)

  return { selected, setSelect, opened, addOpenedFile, delOpenedFile }
}
```

### 4. Create the Sidebar

Now, time to create the sidebar. I break the sidebar into 2 parts: 
- one for a header - that contains a button and project name
- the other for `<NavFiles>` - a list of files/folders that loaded by clicking the above button

```javascript
// src/components/Sidebar.tsx
import { useState } from "react";
import { IFile } from "../types";
import { open } from "@tauri-apps/api/dialog";
// we're gonna create <NavFiles> and `filesys.ts` belows
import NavFiles from "./NavFiles";
import { readDirectory } from "../helpers/filesys";

export default function Sidebar() {
  const [projectName, setProjectName] = useState("");
  const [files, setFiles] = useState<IFile[]>([]);

  const loadFile = async () => {
    const selected = await open({
      directory: true
    })

    if (!selected) return;

    setProjectName(selected as string)
    // .readDirectory accepts a folder path and return 
    // a list of files / folders that insides it
    readDirectory(selected + '/').then(files => {
      console.log(files)
      setFiles(files)
    })
  }

  return <aside id="sidebar" className="w-60 shrink-0 h-full bg-darken">
    <div className="sidebar-header flex items-center justify-between p-4 py-2.5">
      <button className="project-explorer" onClick={loadFile}>File explorer</button>
      <span className="project-name whitespace-nowrap text-gray-400 text-xs">{projectName}</span>  
    </div>
    <div className="code-structure">
      <NavFiles visible={true} files={files}/>
    </div>
  </aside>
}
```

Prepare a `<FileIcon />` to show file thumbnail. Checkout the github project to get all assets

```javascript
// src/components/FileIcon.tsx
// assets link: https://github.com/hudy9x/huditor/tree/main/src/assets
import html from '../assets/html.png';
import css from '../assets/css.png';
import react from '../assets/react.png';
import typescript from '../assets/typescript.png';
import binary from '../assets/binary.png';
import content from '../assets/content.png';
import git from '../assets/git.png';
import image from '../assets/image.png';
import nodejs from '../assets/nodejs.png';
import rust from '../assets/rust.png';
import js from '../assets/js.png';

interface Icons {
  [key: string]: string
}

const icons: Icons = {
  tsx: react,
  css: css,
  svg: image,
  png: image,
  icns: image,
  ico: image,
  gif: image,
  jpeg: image,
  jpg: image,
  tiff: image,
  bmp: image,
  ts: typescript,
  js,
  json: nodejs,
  md: content,
  lock: content,
  gitignore: git,
  html: html,
  rs: rust,
};

interface IFileIconProps {
  name: string;
  size?: 'sm' | 'base'
}

export default function FileIcon({ name, size = 'base' }: IFileIconProps) {
  const lastDotIndex = name.lastIndexOf('.')
  const ext = lastDotIndex !== -1 ? name.slice(lastDotIndex + 1).toLowerCase() : 'NONE'
  const cls = size === 'base' ? 'w-4' : 'w-3';

  if (icons[ext]) {
    return <img className={cls} src={icons[ext]} alt={name} />
  }

  return <img className={cls} src={binary} alt={name} />
}
```
Alright, now we just renders all files that passed from `<Sidebar/>`. Create a file called `src/components/NavFiles.tsx`. Inside the file, we should split the code into 2 parts: render file and render folder.

I'll explain the folder view (`<NavFolderItem/>`) later. About render file, we only need to list all file and provide for each file an action.

When user click on file, not folder, call `onShow` add pass file id to `opened` state in context using `addOpenedfile` function

```javascript
// src/components/NavFiles.tsx

import { MouseEvent } from "react"
import { useSource } from "../context/SourceContext"
import { IFile } from "../types"
import FileIcon from "./FileIcon"
import NavFolderItem from "./NavFolderItem" // this will be defined later

interface Props {
  files: IFile[]
  visible: boolean
}

export default function NavFiles({files, visible}: Props) {
  const {setSelect, selected, addOpenedFile} = useSource()
  
  const onShow = async (ev: React.MouseEvent<HTMLDivElement, MouseEvent>, file: IFile) => {

    ev.stopPropagation();

    if (file.kind === 'file') {
      setSelect(file.id)
      addOpenedFile(file.id)
    }

  }

  return <div className={`source-codes ${visible ? '' : 'hidden'}`}>
    {files.map(file => {
      const isSelected = file.id === selected;

      if (file.kind === 'directory') {
        return <NavFolderItem active={isSelected} key={file.id} file={file} />
      }

      return <div onClick={(ev) => onShow(ev, file)}
        key={file.id}
        className={`soure-item ${isSelected ? 'source-item-active' : ''} flex items-center gap-2 px-2 py-0.5 text-gray-500 hover:text-gray-400 cursor-pointer`}
      >
        <FileIcon name={file.name} />
        <span>{file.name}</span>
      </div>
    })}
  </div>
}
```

### 5. Create helpers/filesys.ts

OK, let's create `src/helpers/filesys.ts` - this is our bridge from frontend to Tauri core. Let me clarify this, as you may know, javascript api does not support reading/writing files/folders for now (tried File System Access API, but can't create folder, delete file, delete folder). So we have to do this in rust

And the only way to communicate with rust code is that using Tauri commands. The below diagram illustrates how Tauri command works 

![rust-command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f30icftmurzbsk4ezzjo.png)

Tauri supports a function called `invoke`. Let import that function from `@tauri-apps/api/tauri` and call to `get_file_content`, `write_file`, and `open_folder` commands




```javascript
// src/helpers/filesys.ts
import { invoke } from "@tauri-apps/api/tauri"
import { nanoid } from "nanoid"
import { saveFileObject } from "../stores/file" // we'll defines this file below
import { IFile } from "../types"

export const readFile = (filePath: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    // get the file content
    invoke("get_file_content", {filePath}).then((message: unknown) => {
      resolve(message as string);
    }).catch(error => reject(error))
  })
}

export const writeFile = (filePath: string, content: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    // write content in file or create a new one
    invoke("write_file", { filePath, content }).then((message: unknown) => {
      if (message === 'OK') {
        resolve(message as string)
      } else {
        reject('ERROR')
      }
    })
  })
}

export const readDirectory = (folderPath: string): Promise<IFile[]> => {
  return new Promise((resolve, reject) => {
    // get all files/folders inside `folderPath`
    invoke("open_folder", { folderPath }).then((message: unknown) => {
      const mess = message as string;
      const files = JSON.parse(mess.replaceAll('\\', '/').replaceAll('//', '/'));
      const entries: IFile[] = [];
      const folders: IFile[] = [];

      if (!files || !files.length) {
        resolve(entries);
        return;
      }

      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        const id = nanoid();
        const entry: IFile = {
          id,
          kind: file.kind,
          name: file.name,
          path: file.path
        }

        if (file.kind === 'file') {
          entries.push(entry)
        } else {
          folders.push(entry)
        }

        // save file metadata to store, i mentioned this above
        // scroll up if any concerns
        saveFileObject(id, entry)

      }

      resolve([...folders, ...entries]);

    })
  })
}

```
### 6. Create stores/files.ts

Our store is very simple, it's an object as follow

```javascript
// src/stores/files.ts
import { IFile } from "../types"

// Ex: {
//   "34sdjwyd3": {
//     "id": "34sdjwyd3",
//     "name": "App.tsx",
//     "kind": "file",
//     "path": "d://path/to/App.tsx",
//   },
//   "872dwehud": {
//     "id": "872dwehud",
//     "name": "components",
//     "kind": "directory",
//     "path": "d://path/to/components",
//   }
// }
interface IEntries {
  [key: string]: IFile
}

const entries: IEntries = {}

export const saveFileObject = (id: string, file: IFile): void => {
  entries[id] = file
}

export const getFileObject = (id: string): IFile => {
  return entries[id]
}
```

### 7. Define Tauri commands

The most important file is here, i've created that before, so you guys can find the full source here [https://github.com/hudy9x/huditor/blob/main/src-tauri/src/fc.rs](https://github.com/hudy9x/huditor/blob/main/src-tauri/src/fc.rs). Belows are shorten version, i won't explain it in details, cuz i'm a very new in rust 

If anyone resolves all warnings in this file, please let me know, i'll update it. Thanks !

```rust
// src-tauri/src/fc.rs
// ...

pub fn read_directory(dir_path: &str) -> String { /**... */ }

pub fn read_file(path: &str) -> String { /**... */ }

pub fn write_file(path: &str, content: &str) -> String { /**... */ }

// These are not used in this tutorial
// i leave it to you guys 😁
pub fn create_directory(path: &str) -> Result<()>{/**... */ }

pub fn remove_file(path: &str) -> Result<()> {/**... */ }

pub fn remove_folder(path: &str) -> Result<()>{ /**... */ }

// ...

```

The only task you guys need to do is define commands. One important thing you have to keep in mind when defining commands and calling them is that:

```javascript
invoke("write_file", {filePath, content}) // frontend
write_file(file_path: &str, content: &str) // tauri command
```
- "write_file" : name must be the same
- params in `invoke` must be object with name in camelCase
- params in tauri command must be snake_case

So, here is our commands

```rust
// src-tauri/src/main.rs

#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

mod fc;

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[tauri::command]
fn open_folder(folder_path: &str) -> String {
    let files = fc::read_directory(folder_path);
    files
}

#[tauri::command]
fn get_file_content(file_path: &str) -> String {
    let content = fc::read_file(file_path);
    content
}

#[tauri::command]
fn write_file(file_path: &str, content: &str) -> String {
    fc::write_file(file_path, content);
    String::from("OK")
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, open_folder, get_file_content, write_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

```

Cool ! the hardest part is done. Let's see whether it works or not 😂

![sidebar-demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dms4a0cqhqk3ov4a8q3j.png)

### 8. Displays files/folders inside subfolder

The last one to complete our Sidebar is displaying files/folders inside subfolder. Create a new file `src/components/NavFolderItem.tsx`. It does some tasks:

1. When user click on folder, reuse `readDirectory` function and show all files/folders
2. Creating new file using `writeFile` function
3. Reuse `<NavFiles />` to display files/folders


```javascript
// src/components/NavFolderItem.tsx

import { nanoid } from "nanoid";
import { useState } from "react";
import { readDirectory, writeFile } from "../helpers/filesys";
import { saveFileObject } from "../stores/file";
import { IFile } from "../types";
import NavFiles from "./NavFiles";

interface Props {
  file: IFile;
  active: boolean;
}

export default function NavFolderItem({ file, active }: Props) {
  const [files, setFiles] = useState<IFile[]>([])
  const [unfold, setUnfold] = useState(false)
  const [loaded, setLoaded] = useState(false)
  const [newFile, setNewFile] = useState(false)
  const [filename, setFilename] = useState('')

  const onShow = async (ev: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
    ev.stopPropagation()

    // if files/foldes are loaded, just show it
    if (loaded) {
      setUnfold(!unfold)
      return;
    }

    const entries = await readDirectory(file.path + '/')

    setLoaded(true)
    setFiles(entries)
    setUnfold(!unfold)
  }

  const onEnter = (key: string) => { 
    if (key === 'Escape') {
      setNewFile(false)
      setFilename('')
      return;
    }

    if (key !== 'Enter') return;

    const filePath = `${file.path}/${filename}`
    
    // Create new file when Enter key pressed
    writeFile(filePath, '').then(() => {
      const id = nanoid();
      const newFile: IFile = {
        id,
        name: filename,
        path: filePath,
        kind: 'file'
      }

      saveFileObject(id, newFile)
      setFiles(prevEntries => [newFile, ...prevEntries])
      setNewFile(false)
      setFilename('')
    })
    
  }

  return <div className="soure-item">
    <div className={`source-folder ${active ? 'bg-gray-200' : ''} flex items-center gap-2 px-2 py-0.5 text-gray-500 hover:text-gray-400 cursor-pointer`}>
      <i className="ri-folder-fill text-yellow-500"></i>
      <div className="source-header flex items-center justify-between w-full group">
        <span onClick={onShow}>{file.name}</span>
        <i onClick={() => setNewFile(true)} className="ri-add-line invisible group-hover:visible"></i>
      </div>
    </div>

      {newFile ? <div className="mx-4 flex items-center gap-0.5 p-2">
      <i className="ri-file-edit-line text-gray-300"></i>
      <input type="text" value={filename} 
        onChange={(ev) => setFilename(ev.target.value)}
        onKeyUp={(ev) => onEnter(ev.key)}
        className="inp"
        />
    </div> : null}

    <NavFiles visible={unfold} files={files} />
  </div>
}
```
Here is the result ! 😍

![sidebar-complete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/boqpi6sdmekguj1o16ci.png)


### 9. Displays file name on tab

Time to show selected files on the tab. It's so simple, just call `useSource` it contains anything we need. Now, create `src/components/CodeArea.tsx`, it has 2 part: a Tab and a content inside

Notice that `codemirror` does not read image files, so we have to create another component to show it

```javascript
import { useRef } from "react"
import { convertFileSrc } from "@tauri-apps/api/tauri"

interface Props {
  path: string;
  active: boolean;
}

export default function PreviewImage({ path, active }: Props) {
  const imgRef = useRef<HTMLImageElement>(null)

  return <div className={`${active ? '' : 'hidden'} p-8`}>
    <img ref={imgRef} src={convertFileSrc(path)} alt="" />
  </div>
}

```

Then import `<PreviewImage/>` into `<CodeArea/>`

```javascript
// src/components/CodeArea.tsx

import { IFile } from "../types"
import { useSource } from "../context/SourceContext"
import { getFileObject } from "../stores/file"
import FileIcon from "./FileIcon"
import useHorizontalScroll from "../helpers/useHorizontalScroll" // will be define later
import PreviewImage from "./PreviewImage"
import CodeEditor from "./CodeEditor" // will be define later

export default function CodeArea() {
  const { opened, selected, setSelect, delOpenedFile } = useSource()
  const scrollRef = useHorizontalScroll()
  const onSelectItem = (id: string) => {
    setSelect(id)
  }

  const isImage = (name: string) => {
    return ['.png', '.gif', '.jpeg', '.jpg', '.bmp'].some(ext => name.lastIndexOf(ext) !== -1) 
  }

  const close = (ev: React.MouseEvent<HTMLElement, MouseEvent>, id: string) => {
    ev.stopPropagation()
    delOpenedFile(id)
  }

  return <div id="code-area" className="w-full h-full">

    {/** This area is for tab bar */}

    <div ref={scrollRef} className="code-tab-items flex items-center border-b border-stone-800 divide-x divide-stone-800 overflow-x-auto">
      {opened.map(item => {
        const file = getFileObject(item) as IFile;
        const active = selected === item ? 'bg-darken text-gray-400' : ''

        return <div onClick={() => onSelectItem(file.id)} className={`tab-item shrink-0 px-3 py-1.5 text-gray-500 cursor-pointer hover:text-gray-400 flex items-center gap-2 ${active}`} key={item}>
          <FileIcon name={file.name} size="sm" />
          <span>{file.name}</span>
          <i onClick={(ev) => close(ev, item)} className="ri-close-line hover:text-red-400"></i>
        </div>
      })}
    </div>

    {/** This area is for code content */}

    <div className="code-contents">
      {opened.map(item => {
        const file = getFileObject(item) as IFile;
        if (isImage(file.name)) {
          return <PreviewImage path={file.path} active={item === selected} />
        }

        return <CodeEditor key={item} id={item} active={item===selected} />

        })}
    </div>
  </div>
}

```

For ease of scrolling, I wrote an hook to scroll the tab bar horizontally

```javascript
// src/helpers/useHorizontalScroll.ts
import { useRef, useEffect } from "react"

export default function useHorizontalScroll() {
  const ref = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    const elem = ref.current
    const onWheel = (ev: WheelEvent) => {
      if (!elem || ev.deltaY === 0) return;

      elem.scrollTo({
        left: elem.scrollLeft + ev.deltaY,
        behavior: 'smooth'
      })
    }

    elem && elem.addEventListener('wheel', onWheel)

    return () => {
      elem && elem.removeEventListener('wheel', onWheel)
    }
  }, [])

  return ref;

}
```

![tab-items](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snwe06xxfvmg3bd9plor.png)


### 10. Show up the file content

Let's finish the work now

```javascript
// src/components/CodeEditor.tsx

import { nanoid } from "nanoid";
import { useEffect, useMemo, useRef } from "react";
import { getFileObject } from "../stores/file";
import { readFile, writeFile } from "../helpers/filesys";

// these packages will be used for codemirror
import { EditorView, basicSetup } from "codemirror";
// hightlight js, markdown, html, css, json, ...
import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { json } from "@codemirror/lang-json";
import { rust } from "@codemirror/lang-rust";
// codemirror theme in dark
import { materialDark } from "cm6-theme-material-dark";

interface Props {
  id: string;
  active: boolean;
}

export default function CodeEditor({ id, active }: Props) {
  const isRendered = useRef(0)
  const editorId = useMemo(() => nanoid(), [])
  const visible = active ? '' : 'hidden'
  const editorRef = useRef<EditorView | null>(null)

  // get file metadata by id from /stores/file.ts
  const updateEditorContent = async (id: string) => {
    const file = getFileObject(id);
    const content = await readFile(file.path)

    fillContentInEditor(content)

  }

  // fill content into codemirror
  const fillContentInEditor = (content: string) => {
    const elem = document.getElementById(editorId)

    if (elem && isRendered.current === 0) {
      isRendered.current = 1;
      editorRef.current = new EditorView({
        doc: content,
        extensions: [
          basicSetup,
          javascript(), markdown(), html(), css(), json(), rust(),
          materialDark
        ],
        parent: elem
      })
    }
  }

  // save the content when pressing Ctrl + S
  const onSave = async () => {
    if (!editorRef.current) return;

    // get codemirror's content
    // if any other way to get content, please let me know in the comment section
    const content = editorRef.current.state.doc.toString();
    const file = getFileObject(id)

    writeFile(file.path, content)
  }

  useEffect(() => {
    updateEditorContent(id)
  }, [id])

  return <main className={`w-full overflow-y-auto ${visible}`} style={{ height: 'calc(100vh - 40px)' }}>
    <div id={editorId} tabIndex={-1} onKeyUp={(ev) => {
      if (ev.ctrlKey && ev.key === 's') {
        ev.preventDefault()
        ev.stopPropagation()
        onSave()
      }
    }}></div>

  </main>

}

```

Finally, let's see our final masterpiece 🤣

![finish](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bnwsyfaekt7nlylnumv5.png)


## Conclusion

Phew, that's a long tutorial, right?. However, i hope it useful for you guys. I still leaves some functions to you to complete such as: create folder, delete file. Feel free to build your own code editor

Lastly, thank for taking your time to read the tutorial. If any question, concern, suggestion please let me know in the commen section. Especially about `rust`, cuz i'm a new to this language