Как я клонировал простой VSCode с помощью Tauri и ReactJS

Как я клонировал простой VSCode с помощью Tauri и ReactJS

7 ноября 2022 г.

Привет, друзья, это Хади!

Недавно я написал учебник о том, как создать блокнот — настольное приложение — с помощью Tauri. Но, возможно, это не совсем демонстрирует силу Таури. Поэтому я решил создать еще одно приложение — простой редактор кода (очень простая версия, такая как VScode)

Это приложение имеет некоторые основные функции, такие как:

  • чтение файлов и папок, содержащихся в папке проекта
  • создание новых файлов
  • редактирование содержимого файла
  • отображение открытых файлов во вкладке
  • показывает структуру папок
  • значки файлов

Необходимое условие

  • Таури
  • Reactjs
  • Понимание того, как работает React Context

Конечный результат:

final-result

Исходный код

Ссылка для скачивания (только для Windows)< /p>

Если вы используете macOS, клонируйте репозиторий и выполните команду сборки следующим образом:

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

Вот версия видеоурока на YouTube:

https://www.youtube.com/embed/LUcUn-_KVXo?embedable=true

Структура кода

Ниже приведены основные пакеты, которые мы будем использовать в этом руководстве.

Объяснение потока кода

Прежде чем перейти к коду, вам следует ознакомиться с обзором того, как работает редактор кода. См. изображение ниже. Вы увидите, что наш редактор кода состоит из 4 основных частей: заголовка, боковой панели, вкладки и редактора

.

Main code parts

Каждая часть эквивалентна одному конкретному компоненту reactjs. Например: вкладка будет компонентом <Tab/>, строка заголовка будет компонентом <Titlebar/> и т. д.

Далее на изображении показано, как наш код запускается после того, как пользователь загружает папку проекта или щелкает файл/папку.

Code flow

Изначально пользователи загружают папку проекта на (1) боковую панель. Все файлы/папки будут сохранены в (3) хранилищах — будут сохранены только метаданные, а не содержимое файла.

Далее, каждый раз, когда пользователь щелкает файл, идентификатор файла будет передаваться в (2) SourceContext — наше управление состоянием.

Когда (2) получите новый selected_file, автоматически (4) добавит новую вкладку с именем файла и (5) отобразит содержимое выбранного файла. Конец

Ядро нашего редактора — самые важные файлы

Чтобы прочитать содержимое папки (например, файлы, папки) и получить содержимое файла, основными являются следующие файлы:

  1. helpers/filesys.ts — содержит функции, вызывающие команды tauri для чтения папки, получения содержимого файла, ... из main.rs
  2. src-tauri/src/main.rs — определяет команды tauri и вызывает функции из fc.rs
  3. src-tauri/src/fc.rs — содержит основные функции для чтения папки, получения содержимого файла, ...
  4. stores/file.ts — хранит метаданные файла

Хорошо, хватит, приступим

Время кодирования

Запустить режим разработки

$ yarn tauri dev

1. Кодовая база скаффолдинга

Первый шаг — инициализация базы кода проекта. Запустите следующие команды и не забудьте выбрать менеджер пакетов — у меня yarn

$ npm create tauri-app huditor

Установите несколько пакетов

$ 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

В этом уроке я буду использовать tailwindcss для стилизации

$ yarn add tailwindcss postcss autoprefixer

После установки перейдите к этому официальному руководству по завершению установки

.

Чтобы сэкономить время, я создал файл стиля здесь.

2. Настройте строку заголовка

Хорошо, первое, что мы собираемся создать, — это заголовок. Это самая простая часть урока. Это потому, что у него всего 3 основные функции, и мы можем использовать appWindow внутри пакета @tauri-apps/api/window для создания этих функций.

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. Создайте исходный контекст

Как я упоминал выше, для того, чтобы компоненты взаимодействовали друг с другом, нам нужен менеджер состояний — <SourceContext/>. Просто сначала определите интерфейс IFile

// src/types/file.ts

export interface IFile {
  id: string;
  name: string;
  kind: 'file' | 'directory';
  path: string;  // d://path/to/file
}

Создайте файл с именем src/context/SourceContext.tsx. Он содержит 2 основных свойства:

  1. selected – сохраняет идентификатор файла, на который нажимает пользователь.
  2. opened — содержит список идентификаторов файлов. Пример: ['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) => { }
});

Создайте <SourceProvider/> для передачи наших состояний дочерним компонентам

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

Кроме того, я также создаю хук под названием useSource, который упрощает использование этих вышеперечисленных состояний и функций

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

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

4. Создайте боковую панель

Теперь пора создать боковую панель. Я разбиваю боковую панель на 2 части:

  • один для заголовка, который содержит кнопку и название проекта
  • другой для <NavFiles> — список файлов/папок, загружаемых при нажатии кнопки выше

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

Подготовьте <FileIcon /> для отображения эскиза файла. Оформить заказ на GitHub, чтобы получить все ресурсы

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

Хорошо! Теперь мы просто отображаем все файлы, переданные из <Sidebar/>.

Создайте файл с именем src/components/NavFiles.tsx. Внутри файла мы должны разделить код на 2 части: файл рендера и папку рендера.

Я объясню представление папки (<NavFolderItem/>) позже. Что касается файла рендеринга, нам нужно только перечислить все файлы и указать действие для каждого файла.

Когда пользователь щелкает файл, а не папку, вызовите onShow, чтобы добавить идентификатор файла передачи в состояние opened в контексте с помощью функции addOpenedfile

// 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. Создайте helpers/filesys.ts

Хорошо, давайте создадим src/helpers/filesys.ts — это наш мост от внешнего интерфейса к ядру Tauri. Позвольте мне уточнить это, как вы, возможно, знаете, API javascript пока не поддерживает чтение/запись файлов/папок (попробовал API доступа к файловой системе, но не может создать папку, удалить файл или удалить папку). Так что мы должны сделать это в ржавчине

И единственный способ общаться с кодом ржавчины — с помощью команд Tauri. На приведенной ниже диаграмме показано, как работают команды Tauri.

Tauri commands

Tauri поддерживает функцию invoke. Давайте импортируем эту функцию из @tauri-apps/api/tauri и вызовем get_file_content, write_file и open_folder. команды.

// 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. Создать магазины/файлы.ts

Наш магазин очень простой, это объект следующим образом:

// 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. Определить команды Tauri

Самый важный файл находится здесь, я уже создавал его ранее, так что вы можете найти полный исходный код здесь https://github.com/hudy9x/huditor/blob/main/src-tauri/src/fc.rs. Ниже приведены укороченные версии, я не буду подробно объяснять это, потому что я очень новичок в ржавчине, и я все еще пытаюсь отработать изломы.

Если кто-нибудь устранит все предупреждения в этом файле, сообщите мне, и я обновлю его. Спасибо!

// 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<()>{ /**... */ }

// ...

Единственная задача, которую вам, ребята, нужно сделать, это определить команды. Одна важная вещь, которую вы должны иметь в виду при определении команд и их вызове, заключается в следующем:

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

* "write_file": имя должно совпадать * параметры в invoke должны быть объектом с именем в camelCase * параметры в команде tauri должны быть змеиным_case

Итак, вот наши команды

// 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");
}

Прохладный ! самое сложное сделано. Посмотрим, получится или нет 😂

sidebar-demo

8. Отображает файлы/папки внутри подпапки

Последнее, что нужно сделать, чтобы наша боковая панель отображала файлы/папки внутри подпапок. Создайте новый файл src/components/NavFolderItem.tsx. Он выполняет некоторые задачи:

  1. Когда пользователь нажимает на папку, повторно использовать функцию readDirectory и показывать все файлы/папки
  2. Создание новых файлов с помощью функции writeFile
  3. Повторно используйте <NavFiles /> для отображения файлов/папок

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

Вот результат! 😍

sidebar-complete

9. Отображает имя файла на вкладке

Время показать выбранные файлы на вкладке. Это так просто, просто вызовите useSource, он содержит все, что нам нужно. Теперь создайте src/components/CodeArea.tsx, он состоит из 2 частей: вкладки и содержимого внутри.

Обратите внимание, что codemirror не читает файлы изображений, поэтому нам нужно создать еще один компонент, чтобы показать его

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

Затем импортируйте <PreviewImage/> в <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>
}

Для удобства прокрутки я написал хук для горизонтальной прокрутки панели вкладок

// 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. Показать содержимое файла

Давайте закончим работу сейчас

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

}

Наконец, давайте посмотрим на наш финальный шедевр 🤣

finish

Заключение

Уф, это длинное руководство, верно? Тем не менее, я надеюсь, что это полезно для вас, ребята. Я по-прежнему оставляю вам некоторые функции, такие как: создание папки и удаление файла. Вы можете создать свой собственный редактор кода.

Наконец, спасибо, что нашли время, чтобы прочитать учебник. Если у вас есть какие-либо вопросы, проблемы или предложения, пожалуйста, дайте мне знать в разделе комментариев. Особенно о rust, как я уже сказал, я новичок в этом языке


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE