Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement auto-save for files #309

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 16 additions & 46 deletions src/components/workspace/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Tree } from '@/interfaces/workspace.interface';
import { configureMonacoEditor } from '@/utility/editor';
import EventEmitter from '@/utility/eventEmitter';
import { delay, fileTypeFromFileName } from '@/utility/utils';
import EditorDefault from '@monaco-editor/react';
import EditorDefault, { OnChange } from '@monaco-editor/react';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { FC, useEffect, useRef, useState } from 'react';
// import { useLatest } from 'react-use';
Expand All @@ -24,7 +24,7 @@ interface Props {
const Editor: FC<Props> = ({ className = '' }) => {
const { activeProject } = useProject();
const { getFile, saveFile: storeFileContent } = useFile();
const { fileTab, updateFileDirty } = useFileTab();
const { fileTab } = useFileTab();
const { theme } = useTheme();

const { isFormatOnSave, getSettingStateByKey } = useSettingAction();
Expand All @@ -36,15 +36,14 @@ const Editor: FC<Props> = ({ className = '' }) => {
]);
const editorMode = getSettingStateByKey('editorMode');

// Using this extra state to trigger save file from js event
const [saveFileCounter, setSaveFileCounter] = useState(1);
const latestFile = useLatest(fileTab.active);

const [initialFile, setInitialFile] = useState<Pick<
Tree,
'id' | 'content'
> | null>(null);

const autoSaveTimeoutRef = useRef<number | null>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const vimStatusBarRef = useRef<HTMLElement | null>(null);
Expand All @@ -55,16 +54,14 @@ const Editor: FC<Props> = ({ className = '' }) => {
// eslint-disable-next-line prefer-const
let lspWebSocket: ReconnectingWebSocket | null;

const saveFile = async () => {
const fileContent = editorRef.current?.getValue() ?? '';
const saveFile = async (value?: string) => {
if (!editorRef.current) return;

const fileContent = value ?? editorRef.current.getValue();
if (!fileContent || !fileTab.active) return;
try {
if (isFormatOnSave()) {
editorRef.current?.trigger(
'editor',
'editor.action.formatDocument',
{},
);
editorRef.current.trigger('editor', 'editor.action.formatDocument', {});
await delay(200);
}
await storeFileContent(fileTab.active.path, fileContent);
Expand All @@ -74,10 +71,6 @@ const Editor: FC<Props> = ({ className = '' }) => {
}
};

const updateFileSaveCounter = () => {
setSaveFileCounter((prev) => prev + 1);
};

useEffect(() => {
if (!fileTab.active) return;

Expand All @@ -88,24 +81,9 @@ const Editor: FC<Props> = ({ className = '' }) => {
.catch(() => {});
}, []);

useEffect(() => {
// Don't save file on initial render
if (saveFileCounter === 1) return;

const saveFileDebouce = setTimeout(() => {
(async () => {
await saveFile();
})().catch(() => {});
}, 300);

return () => {
clearTimeout(saveFileDebouce);
};
}, [saveFileCounter]);

useEffect(() => {
if (!isLoaded) return;
EventEmitter.on('SAVE_FILE', updateFileSaveCounter);
EventEmitter.on('SAVE_FILE', saveFile);

// If file is changed e.g. in case of build process then force update in editor
EventEmitter.on('FORCE_UPDATE_FILE', (file) => {
Expand All @@ -125,7 +103,7 @@ const Editor: FC<Props> = ({ className = '' }) => {
});
});
return () => {
EventEmitter.off('SAVE_FILE', updateFileSaveCounter);
EventEmitter.off('SAVE_FILE', saveFile);
EventEmitter.off('FORCE_UPDATE_FILE');
};
}, [isLoaded]);
Expand Down Expand Up @@ -154,21 +132,13 @@ const Editor: FC<Props> = ({ className = '' }) => {
editorRef.current.focus();
};

const markFileDirty = () => {
const handleEditorChange: OnChange = (value) => {
if (!editorRef.current) return;
const fileContent = editorRef.current.getValue();
if (
fileTab.active?.path !== initialFile?.id ||
!initialFile?.content ||
initialFile.content === fileContent
) {
return;
}
if (!fileContent) {
return;

if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
if (!fileTab.active?.path) return;
updateFileDirty(fileTab.active.path, true);
autoSaveTimeoutRef.current = window.setTimeout(() => saveFile(value), 1000);
};

const initializeEditorMode = async () => {
Expand Down Expand Up @@ -243,7 +213,7 @@ const Editor: FC<Props> = ({ className = '' }) => {
// height="90vh"
defaultLanguage={fileTypeFromFileName(fileTab.active?.path ?? '')}
defaultValue={undefined}
onChange={markFileDirty}
onChange={handleEditorChange}
options={editorOptions}
onMount={(editor, monaco) => {
(async () => {
Expand Down
8 changes: 0 additions & 8 deletions src/components/workspace/ExecuteFile/ExecuteFile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import AppIcon, { AppIconType } from '@/components/ui/icon';
import { useFileTab } from '@/hooks';
import { useLogActivity } from '@/hooks/logActivity.hooks';
import { useProjectActions } from '@/hooks/project.hooks';
import { useProject } from '@/hooks/projectV2.hooks';
Expand Down Expand Up @@ -38,7 +37,6 @@ const ExecuteFile: FC<Props> = ({
const { projectFiles } = useProject();
const { compileFuncProgram, compileTactProgram } = useProjectActions();
const { createLog } = useLogActivity();
const { hasDirtyFiles } = useFileTab();
const [selectedFile, setSelectedFile] = useState<Tree | undefined>();
const selectedFileRef = useRef<Tree | undefined>();
const isAutoBuildAndDeployEnabled =
Expand All @@ -54,12 +52,6 @@ const ExecuteFile: FC<Props> = ({
});

const buildFile = async (e: ButtonClick) => {
if (hasDirtyFiles()) {
message.warning({
content: 'You have unsaved changes',
key: 'unsaved_changes',
});
}
const selectedFile = selectedFileRef.current;
if (!selectedFile) {
createLog('Please select a file', 'error');
Expand Down
24 changes: 2 additions & 22 deletions src/components/workspace/Tabs/Tabs.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,8 @@
}
&:hover {
.close {
&:not(.isDirty) {
.closeIcon {
visibility: visible !important;
}
}
&:hover {
&.isDirty {
.fileDirtyIcon {
visibility: hidden;
}
.closeIcon {
visibility: visible;
}
}
.closeIcon {
visibility: visible !important;
}
}
}
Expand All @@ -80,14 +68,6 @@
background-color: #f7f7f7;
}
}
&.isDirty {
.closeIcon {
visibility: hidden;
}
.fileDirtyIcon {
visibility: visible;
}
}
}

.fileDirtyIcon {
Expand Down
15 changes: 3 additions & 12 deletions src/components/workspace/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const contextMenuItems: IContextMenuItems['items'] = [
];

const Tabs: FC = () => {
const { fileTab, open, close, rename, updateFileDirty } = useFileTab();
const { fileTab, open, close, rename } = useFileTab();

const closeTab = (e: React.MouseEvent, filePath: string) => {
e.preventDefault();
Expand All @@ -58,12 +58,6 @@ const Tabs: FC = () => {
},
[close],
);
const onFileSave = useCallback(
({ filePath }: { filePath: string }) => {
updateFileDirty(filePath, false);
},
[updateFileDirty],
);

const onFileRename = useCallback(
({ oldPath, newPath }: IRenameFile) => {
Expand All @@ -87,24 +81,21 @@ const Tabs: FC = () => {

const renderCloseButton = (item: ITabItems) => (
<span
className={cn(s.close, { [s.isDirty]: item.isDirty })}
className={s.close}
onClick={(e) => {
closeTab(e, item.path);
}}
>
{item.isDirty && <span className={s.fileDirtyIcon}></span>}
<AppIcon name="Close" className={s.closeIcon} />
</span>
);

useEffect(() => {
EventEmitter.on('FILE_SAVED', onFileSave);
EventEmitter.on('FILE_RENAMED', onFileRename);
return () => {
EventEmitter.off('FILE_SAVED', onFileSave);
EventEmitter.off('FILE_RENAMED', onFileRename);
};
}, [updateFileDirty]);
}, []);

if (fileTab.items.length === 0) {
return <></>;
Expand Down
23 changes: 1 addition & 22 deletions src/hooks/fileTabs.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
DEFAULT_PROJECT_SETTING,
updateProjectTabSetting,
} from '@/utility/projectSetting';
import cloneDeep from 'lodash.clonedeep';
import { useContext } from 'react';

const useFileTab = () => {
Expand All @@ -31,7 +30,7 @@ const useFileTab = () => {
}),
);
} else {
const newTab = { name, path, isDirty: false, type };
const newTab = { name, path, type };
const updatedTab = {
...fileTab,
items: [...fileTab.items, newTab],
Expand Down Expand Up @@ -88,31 +87,11 @@ const useFileTab = () => {
EventEmitter.emit('FORCE_UPDATE_FILE', newPath);
};

const updateFileDirty = async (filePath: string, isDirty: boolean) => {
const updatedItems = cloneDeep(fileTab).items.map((item) => {
if (item.path === filePath) {
return { ...item, isDirty: isDirty };
}
return item;
});

const updatedTab = { ...fileTab, items: updatedItems };
updateTabs(
await updateProjectTabSetting(activeProject?.path as string, updatedTab),
);
};

const hasDirtyFiles = () => {
return fileTab.items.some((item) => item.isDirty);
};

return {
fileTab,
open,
close,
rename,
updateFileDirty,
hasDirtyFiles,
};
};

Expand Down
1 change: 0 additions & 1 deletion src/interfaces/workspace.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export interface Tree {
isOpen?: boolean;
path: string;
content?: string;
isDirty?: boolean;
createdAt?: Date;
updatedAt?: Date;
}
Expand Down
2 changes: 1 addition & 1 deletion src/schemas/fileTab.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const iTabItemsSchema = z.object({
name: z.string(),
path: z.string(),
type: z.union([z.literal('default'), z.literal('git')]).default('default'),
isDirty: z.boolean(),
isDirty: z.boolean().optional(),
});

const activeTabSchema = z.preprocess(
Expand Down
1 change: 0 additions & 1 deletion src/state/IDE.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export interface ITabItems {
name: string;
path: string;
type: 'default' | 'git';
isDirty: boolean;
}

export interface IFileTab {
Expand Down
2 changes: 1 addition & 1 deletion src/utility/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface EventEmitterPayloads {
LOG_CLEAR: undefined;
LOG: LogEntry | string | Uint8Array;
ON_SPLIT_DRAG_END: { position?: number };
SAVE_FILE: undefined | { fileId: string; content: string };
SAVE_FILE: undefined;
FORCE_UPDATE_FILE: string | { oldPath: string; newPath: string };
FILE_SAVED: { filePath: string };
FILE_RENAMED: { oldPath: string; newPath: string };
Expand Down
1 change: 0 additions & 1 deletion src/utility/projectSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export async function updateProjectTabSetting(
{
name: defaultFilePath.split('/').pop() ?? defaultFilePath,
path: defaultFilePath,
isDirty: false,
type: 'default',
},
],
Expand Down