paint-brush
How I Cloned a Simple VSCode Using Tauri and ReactJSby@hudy
8,001 reads
8,001 reads

How I Cloned a Simple VSCode Using Tauri and ReactJS

by hudyNovember 6th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Tauri is a basic version of VScode, a simple version of a basic web-based code editor. The code editor has 4 main parts: Titlebar, Sidebar, Tab, Tab and Editor. It runs after the user loads a project folder or clicks on a file/ folder. It has some basic functions such as: reading files and folders, creating new files, editing file content and displaying opened files in tab icons. It's a simple app - a simple code editor (a very basic version like VSCode)

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How I Cloned a Simple VSCode Using Tauri and ReactJS
hudy HackerNoon profile picture


Hi friends, it's Hudy!


Recently, I wrote a tutorial about how to build a notebook - desktop application - using Tauri. But, maybe that doesn’t really demonstrate the power of Tauri. As such, 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 are contained in a project folder
  • creating new files
  • editing file content
  • displaying opened files in tab
  • showing folder structure
  • file icons

Prerequisite

  • Tauri
  • Reactjs
  • Understanding how React Context works


Final result:


final-result

Source code

Download link (only for windows)


If you're using macOS, please clone the repo and run the build command as follows:


// make sure that you installed Tauri already
$ yarn tauri build


Here is the youtube video tutorial version:

Code structure

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


Below are the main packages that we're gonna use in this tutorial

  • codemirror - a code editor component for the web
  • remixicon - a simply delightful icon system, but only use several ones 🤣
  • tailwindcss - a utility-first CSS framework
  • nanoid - unique string ID generator
  • 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


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

Next, the image illustrates how our code runs after the user loads a project folder or clicks on a file/folder.


Code flow

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


Next, every time a user clicks on a file, the 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 the file's name, and (5) display the selected file's content. End

The core of our editor - the most important files

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


  1. helpers/filesys.ts - contains functions that call tauri commands for reading the folder, getting file content, ... from main.rs
  2. src-tauri/src/main.rs - defines tauri commands, and calls functions from fc.rs
  3. src-tauri/src/fc.rs - contains main functions for reading the 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


$ yarn tauri dev

1. Scaffolding code base

The first step, initialize the project code base. Run the following commands and remember to select a package manager - mine is yarn


$ npm create tauri-app huditor


Install some packages


$ 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


$ yarn add tailwindcss postcss autoprefixer


After installing, head to this official guide for completing the installation

To save time, I created a style file here.

2. Customize the Titlebar

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


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 for components to communicate with each other, we need a state manager - <SourceContext/>. Just define the IFile interface first


// 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 clicks on

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


// 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


// 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 states and function easier


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


// 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


// 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 render 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 files and provide an action for each file.


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


// 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 the 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 a folder, delete a file, or delete a folder). So we have to do this in rust


And the only way to communicate with rust code is with Tauri commands. The below diagram illustrates how Tauri commands work.

Tauri commands


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


// 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 follows:


// 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. Below are shortened versions, I won't explain it in detail, because I'm very new to rust and I’m still trying to workout the kinks.


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


// 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:


invoke("write_file", {filePath, content}) // frontend
write_file(file_path: &str, content: &str) // tauri command


  • "write_file"s: name must be the same
  • params in invoke must be an object with the name in camelCase
  • params in tauri command must be snake_case

So, here are our commands


// 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

8. Displays files/folders inside a 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 the user clicks on the folder, reuse readDirectory function and show all files/folders
  2. Creating new files using writeFile function
  3. Reuse <NavFiles /> to display files/folders


// 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

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 parts: a Tab and content inside.

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


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/>


// 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 a hook to scroll the tab bar horizontally


// 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

10. Show up the file content

Let's finish the work now


// 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

Conclusion

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


Lastly, thank you for taking the time to read the tutorial. If you have any questions, concerns, or suggestions please let me know in the comment section. Especially about rust, like I said, I'm new to this language