Skip to content

Commit

Permalink
feat: keep code after close app
Browse files Browse the repository at this point in the history
Application will save code if there is no edit operation in 1 second.
File will save in `appDataDir/files/*.dat`
  • Loading branch information
mslxl committed Dec 8, 2023
1 parent ed31a80 commit 313310f
Show file tree
Hide file tree
Showing 17 changed files with 258 additions and 92 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Yet another IDE for competitive programming
+ [X] C++
+ [ ] Python
- [X] Coding
- [X] Keep codes after close app
- [X] Compile, Run and Check
+ [X] C++
+ [X] Python
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"clsx": "^2.0.0",
"codemirror": "^6.0.1",
"codemirror-languageserver": "^1.11.0",
"crc": "^4.3.2",
"events": "^3.3.0",
"framer-motion": "^10.16.5",
"immer": "^10.0.3",
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions src/components/codemirror/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import { basicSetup } from "codemirror"
import { LspProvider } from "./language"
import { Extension } from "@codemirror/state"
import { KeymapProvider } from "./keymap"
import { Atom, PrimitiveAtom, useSetAtom } from "jotai"
import { Atom, PrimitiveAtom } from "jotai"
import { Source } from "@/store/source"
import { useImmerAtom } from "jotai-immer"
import { concat, map } from "lodash"
import "@fontsource/jetbrains-mono"
import useExtensionCompartment, { generateCommonConfigurationExtension } from "@/hooks/useExtensionCompartment"
import useTimeoutInvoke from "@/hooks/useTimeoutInvoke"
import * as cache from "@/lib/fs/cache"
import { useMitt } from "@/hooks/useMitt"

type CodemirrorProps = {
className?: string
id: number
title: string
sourceAtom: PrimitiveAtom<Source>
changedStatusAtom: PrimitiveAtom<boolean>
lspAtom: Atom<ReturnType<LspProvider>>
keymapAtom: Atom<ReturnType<KeymapProvider>>
}
Expand All @@ -27,7 +31,6 @@ const Codemirror = memo((props: CodemirrorProps) => {
const cm = useRef<EditorView | null>(null)

const [source, patchSource] = useImmerAtom(props.sourceAtom)
const setChangeStatus = useSetAtom(props.changedStatusAtom)

const configurableExtensions = concat(
[
Expand All @@ -37,6 +40,17 @@ const Codemirror = memo((props: CodemirrorProps) => {
generateCommonConfigurationExtension(cm),
)

const [doCacheOnTimeout, , cancelTimeoutCache] = useTimeoutInvoke<never>(() => {
cache.updateCache(props.id, props.title, source)
}, 1000)
useMitt("cache", (id) => {
if (id == props.id || id == -1) doCacheOnTimeout({} as never)
})

useEffect(()=>{
doCacheOnTimeout({} as never)
}, [props.title])

useEffect(() => {
if (parentRef.current == null) return
let isDestroy = false
Expand Down Expand Up @@ -65,10 +79,12 @@ const Codemirror = memo((props: CodemirrorProps) => {
}),
EditorView.updateListener.of((e) => {
if (!e.docChanged) return
// update program status
patchSource((prev) => {
prev.code.source = e.state.doc.toString()
})
setChangeStatus(true)
// cache file
doCacheOnTimeout({} as never)
}),
],
doc: source.code.source ?? "",
Expand All @@ -80,12 +96,13 @@ const Codemirror = memo((props: CodemirrorProps) => {
if (cmClosure != null) {
cmClosure.destroy()
cmClosure = null
cancelTimeoutCache()
} else {
isDestroy = true
}
}
}, [parentRef])

return <div ref={parentRef} className={clsx("flex items-stretch min-w-0", props.className)} />
return <div ref={parentRef} className={clsx("flex items-stretch min-w-0", props.className, `debug-id-${props.id}`)} />
})
export default Codemirror
3 changes: 2 additions & 1 deletion src/components/runner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import EmptyRunner from "./empty"
export default function Runnner({ className }: { className?: string }) {
const activeId = useAtomValue(activeIdAtom)
if (activeId == -1) {
return <EmptyRunner className={className}/>
return <EmptyRunner className={className} />
}
return <RunnerContent className={className} activeIdAtom={activeIdAtom} />
}
Expand Down Expand Up @@ -84,6 +84,7 @@ function RunnerContent(props: { className?: string; activeIdAtom: Atom<number> }
const testcaseList = testcases.map((atom, index) => (
<SingleRunner
key={index}
id={activeId}
sourceAtom={sourceCodeAtom}
testcaseAtom={atom}
onDelete={() => dispatchTestcases({ type: "remove", atom })}
Expand Down
16 changes: 14 additions & 2 deletions src/components/runner/single.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type SingleRunnerProps = {
timeLimitsAtom: Atom<number>
memoryLimitsAtom: Atom<number>
checkerAtom: Atom<string>
id: number
taskId: string
onDelete: () => void
}
Expand Down Expand Up @@ -67,8 +68,17 @@ export default function SingleRunner(props: SingleRunnerProps) {
const inputAtom = useMemo(() => focusAtom(props.testcaseAtom, (optic) => optic.prop("input")), [props.testcaseAtom])
const outputAtom = useMemo(() => focusAtom(props.testcaseAtom, (optic) => optic.prop("output")), [props.testcaseAtom])

const [input, setInput] = useAtom(inputAtom)
const [output, setOutput] = useAtom(outputAtom)
const [input, setInputAtom] = useAtom(inputAtom)
const [output, setOutputAtom] = useAtom(outputAtom)

function setInput(inp: string){
setInputAtom(inp)
emit('cache', props.id)
}
function setOutput(oup: string){
setOutputAtom(oup)
emit('cache', props.id)
}

const [actualStdout, setActualStdout] = useState("")
const [actualStderr, setActualStderr] = useState("")
Expand All @@ -80,6 +90,8 @@ export default function SingleRunner(props: SingleRunnerProps) {
useEffect(() => {
setJudgeStatus("UK")
setCheckerReport("")
setActualStderr("")
setActualStdout("")
}, [props.sourceAtom])

useMitt(
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useMitt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DependencyList, useEffect } from "react"
type Events = {
fileMenu: "new" | "newContest" | "open" | "openContest" | "save" | "saveAs"
run: "all" | string
cache: number
}

const emitter = mitt<Events>()
Expand Down
37 changes: 37 additions & 0 deletions src/hooks/useTimeoutInvoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useRef } from "react"

export default function useTimeoutInvoke<T>(
call: (params: T) => void,
timeout: number,
): [(params: T) => void, (params: T) => void, () => void] {
const timer = useRef<NodeJS.Timeout | null>(null)
const param = useRef<T | null>(null)
const caller = useRef<(params: T)=>void>()

useEffect(()=>{
caller.current = call
}, [call])

function cancel() {
if (timer.current) {
clearTimeout(timer.current)
timer.current = null
}
}
function delay(params: T) {
param.current = params
cancel()
timer.current = setTimeout(() => {
if (timer.current) {
clearTimeout(timer.current)
}
caller.current!(param.current!)
}, timeout)
}
function invokeNow(params: T) {
cancel()
caller.current!(params)
}

return [delay, invokeNow, cancel]
}
58 changes: 58 additions & 0 deletions src/lib/fs/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { path, fs } from "@tauri-apps/api"
import { Source } from "@/store/source"
import * as log from "tauri-plugin-log-api"

async function getDataDir() {
const filesDir = await path.join(await path.appDataDir(), "files")
if (!(await fs.exists(filesDir))) {
await fs.createDir(filesDir)
}
return filesDir
}

async function getFilePath(id: number): Promise<string> {
const dir = await getDataDir()
const file = await path.join(dir, `${id}.dat`)
return file
}

export async function updateCache(id: number, title: string, src: Source) {
const file = await getFilePath(id)
log.info(`update cache ${id} to path ${file}`)
fs.writeTextFile(file, JSON.stringify({ src, title }))
}

export async function dropCache(id: number) {
const file = await getFilePath(id)
log.info(`drop cache ${id} (${file})`)
if ((await fs.exists(file))) {
await fs.removeFile(file)
}
}

export async function dropAll() {
const dir = await getDataDir()
const files = await fs.readDir(dir, { recursive: false })
const tasks = files.map(async (p) => {
if (p.children == null) {
await fs.removeFile(p.path)
}
})
await Promise.all(tasks)
}

async function recoverFromFile(file: string): Promise<[string,Source]> {
const content = await fs.readTextFile(file)
const obj = JSON.parse(content)
return [obj.title, obj.src]
}

export async function recoverAllCache(): Promise<[string, Source][]> {
const dir = await getDataDir()
const files = (await fs.readDir(dir, { recursive: false }))
.filter((p) => p.children == null && p.name != null && p.name.endsWith(".dat"))
.map((e) => e.path!)

const tasks = files.map(recoverFromFile)
return await Promise.all(tasks)
}
6 changes: 3 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { attachConsole } from "tauri-plugin-log-api"
import { loadSettingsStore } from "./store/setting"
import { LanguageMode, isDebug } from "./lib/ipc"
import { useCompetitiveCompanion } from "./hooks/useCompetitiveCompanion"
import { Source, useAddSource } from "./store/source"
import { Source, useAddSources } from "./store/source"

async function maskContextMenu() {
const debug = await isDebug()
Expand All @@ -26,7 +26,7 @@ async function maskContextMenu() {
}

function CompetitiveCompanion() {
const addSource = useAddSource()
const addSources = useAddSources()
useCompetitiveCompanion((p) => {
let title = p.name
let source: Source = {
Expand All @@ -43,7 +43,7 @@ function CompetitiveCompanion() {
testcases: p.tests,
},
}
addSource(title, source)
addSources([{ title, source }])
})
return null
}
Expand Down
9 changes: 3 additions & 6 deletions src/pages/Main/editor-tabpane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useReadAtom from "@/hooks/useReadAtom"
import { LanguageMode } from "@/lib/ipc"
import { keymapExtensionAtom } from "@/store/setting/keymap"
import { clangdPathAtom, pyrightsPathAtom } from "@/store/setting/setup"
import { SourceHeader, activeIdAtom, sourceCodeChangedAtom, sourceStoreAtom } from "@/store/source"
import { SourceHeader, activeIdAtom, sourceStoreAtom } from "@/store/source"
import { Extension } from "@codemirror/state"
import clsx from "clsx"
import { PrimitiveAtom, atom, useAtomValue } from "jotai"
Expand All @@ -23,10 +23,6 @@ export default function EditorTabPanel(props: EditorProps) {
() => focusAtom(sourceStoreAtom, (optic) => optic.prop(header.id).prop("code").prop("language")),
[header.id],
)
const sourceChangedStatusAtom = useMemo(
() => focusAtom(sourceCodeChangedAtom, (optic) => optic.prop(header.id)),
[header.id],
)
const sourceCodeLanguage = useAtomValue(sourceCodeLanguageAtom)

const readClangdPath = useReadAtom(clangdPathAtom)
Expand All @@ -46,7 +42,8 @@ export default function EditorTabPanel(props: EditorProps) {
hidden: active != header.id,
})}
sourceAtom={sourceAtom}
changedStatusAtom={sourceChangedStatusAtom}
id={header.id}
title={header.title}
keymapAtom={keymapExtensionAtom}
lspAtom={lspExtensionAtom}
/>
Expand Down
11 changes: 11 additions & 0 deletions src/pages/Main/event/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import MenuEventReceiver from "./menu-event"
import StatusRecover from "./status-recover"

export default function MainEventRegister() {
return (
<>
<MenuEventReceiver />
<StatusRecover />
</>
)
}
Loading

0 comments on commit 313310f

Please sign in to comment.