Как я клонировал простой VSCode с помощью Tauri и ReactJS
7 ноября 2022 г.Привет, друзья, это Хади!
Недавно я написал учебник о том, как создать блокнот — настольное приложение — с помощью Tauri. Но, возможно, это не совсем демонстрирует силу Таури. Поэтому я решил создать еще одно приложение — простой редактор кода (очень простая версия, такая как VScode)
Это приложение имеет некоторые основные функции, такие как:
- чтение файлов и папок, содержащихся в папке проекта
- создание новых файлов
- редактирование содержимого файла
- отображение открытых файлов во вкладке
- показывает структуру папок
- значки файлов
Необходимое условие
- Таури
- Reactjs
- Понимание того, как работает React Context
Конечный результат:
Ссылка для скачивания (только для Windows)< /p>
Если вы используете macOS, клонируйте репозиторий и выполните команду сборки следующим образом:
// make sure that you installed Tauri already
$ yarn tauri build
Вот версия видеоурока на YouTube:
https://www.youtube.com/embed/LUcUn-_KVXo?embedable=true а>
Структура кода
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
Ниже приведены основные пакеты, которые мы будем использовать в этом руководстве.
- codemirror — компонент редактора кода для Интернета.
- remixicon — просто восхитительная система иконок, но используйте только несколько 🤣
- tailwindcss — CSS-фреймворк, ориентированный на утилиты
- nanoid – генератор уникальных строковых идентификаторов
- cm6-theme-material-dark — основная темная тема
Объяснение потока кода
Прежде чем перейти к коду, вам следует ознакомиться с обзором того, как работает редактор кода. См. изображение ниже. Вы увидите, что наш редактор кода состоит из 4 основных частей: заголовка, боковой панели, вкладки и редактора
.
Каждая часть эквивалентна одному конкретному компоненту reactjs. Например: вкладка будет компонентом <Tab/>
, строка заголовка будет компонентом <Titlebar/>
и т. д.
Далее на изображении показано, как наш код запускается после того, как пользователь загружает папку проекта или щелкает файл/папку.
Изначально пользователи загружают папку проекта на (1) боковую панель. Все файлы/папки будут сохранены в (3) хранилищах — будут сохранены только метаданные, а не содержимое файла.
Далее, каждый раз, когда пользователь щелкает файл, идентификатор файла будет передаваться в (2) SourceContext — наше управление состоянием.
Когда (2) получите новый selected_file
, автоматически (4) добавит новую вкладку с именем файла и (5) отобразит содержимое выбранного файла. Конец
Ядро нашего редактора — самые важные файлы
Чтобы прочитать содержимое папки (например, файлы, папки) и получить содержимое файла, основными являются следующие файлы:
- helpers/filesys.ts — содержит функции, вызывающие команды tauri для чтения папки, получения содержимого файла, ... из
main.rs
- src-tauri/src/main.rs — определяет команды tauri и вызывает функции из
fc.rs
- src-tauri/src/fc.rs — содержит основные функции для чтения папки, получения содержимого файла, ...
- 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 основных свойства:
selected
– сохраняет идентификатор файла, на который нажимает пользователь.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 поддерживает функцию 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");
}
Прохладный ! самое сложное сделано. Посмотрим, получится или нет 😂
8. Отображает файлы/папки внутри подпапки
Последнее, что нужно сделать, чтобы наша боковая панель отображала файлы/папки внутри подпапок. Создайте новый файл src/components/NavFolderItem.tsx
. Он выполняет некоторые задачи:
- Когда пользователь нажимает на папку, повторно использовать функцию
readDirectory
и показывать все файлы/папки - Создание новых файлов с помощью функции
writeFile
- Повторно используйте
<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>
}
Вот результат! 😍
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;
}
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>
}
Наконец, давайте посмотрим на наш финальный шедевр 🤣
Заключение
Уф, это длинное руководство, верно? Тем не менее, я надеюсь, что это полезно для вас, ребята. Я по-прежнему оставляю вам некоторые функции, такие как: создание папки и удаление файла. Вы можете создать свой собственный редактор кода.
Наконец, спасибо, что нашли время, чтобы прочитать учебник. Если у вас есть какие-либо вопросы, проблемы или предложения, пожалуйста, дайте мне знать в разделе комментариев. Особенно о rust
, как я уже сказал, я новичок в этом языке
Оригинал