diff --git a/src/components/Editors/Text/TextTab.ts b/src/components/Editors/Text/TextTab.ts index 8fb10566f..3f372ee0b 100644 --- a/src/components/Editors/Text/TextTab.ts +++ b/src/components/Editors/Text/TextTab.ts @@ -14,9 +14,6 @@ import { wait } from '/@/utils/wait' const throttledCacheUpdate = debounce<(tab: TextTab) => Promise | void>( async (tab) => { - // Updates the isUnsaved status of the tab - tab.updateUnsavedStatus() - if (!tab.editorModel || tab.editorModel.isDisposed()) return const fileContent = tab.editorModel?.getValue() @@ -78,6 +75,13 @@ export class TextTab extends FileTab { ) } + fileDidChange() { + // Updates the isUnsaved status of the tab + this.updateUnsavedStatus() + + super.fileDidChange() + } + async onActivate() { if (this.isActive) return this.isActive = true @@ -121,6 +125,7 @@ export class TextTab extends FileTab { this.disposables.push( this.editorModel?.onDidChangeContent(() => { throttledCacheUpdate(this) + this.fileDidChange() }) ) this.disposables.push( @@ -131,7 +136,9 @@ export class TextTab extends FileTab { this.editorInstance?.layout() } - onDeactivate() { + async onDeactivate() { + await super.onDeactivate() + // MonacoEditor is defined if (this.tabSystem.hasFired) { const viewState = this.editorInstance.saveViewState() diff --git a/src/components/Editors/TreeEditor/Tab.ts b/src/components/Editors/TreeEditor/Tab.ts index e7d8de9b4..ea766da92 100644 --- a/src/components/Editors/TreeEditor/Tab.ts +++ b/src/components/Editors/TreeEditor/Tab.ts @@ -99,7 +99,9 @@ export class TreeTab extends FileTab { async onActivate() { this.treeEditor.activate() } - onDeactivate() { + async onDeactivate() { + await super.onDeactivate() + this._treeEditor?.deactivate() } diff --git a/src/components/Editors/TreeEditor/TreeEditor.ts b/src/components/Editors/TreeEditor/TreeEditor.ts index 190ab7a37..12b9c604b 100644 --- a/src/components/Editors/TreeEditor/TreeEditor.ts +++ b/src/components/Editors/TreeEditor/TreeEditor.ts @@ -73,6 +73,7 @@ export class TreeEditor { this.valueSuggestions = [] this.parent.updateCache() + this.parent.fileDidChange() }) App.getApp().then(async (app) => { diff --git a/src/components/TabSystem/FileTab.ts b/src/components/TabSystem/FileTab.ts index ab148d366..f4cb12824 100644 --- a/src/components/TabSystem/FileTab.ts +++ b/src/components/TabSystem/FileTab.ts @@ -10,9 +10,27 @@ import { } from '../FileSystem/Polyfill' import { download } from '../FileSystem/saveOrDownload' import { writableToUint8Array } from '/@/utils/file/writableToUint8Array' +import { settingsState } from '../Windows/Settings/SettingsState' +import { debounce } from 'lodash-es' export type TReadOnlyMode = 'forced' | 'manual' | 'off' +const shouldAutoSave = async () => { + const app = await App.getApp() + + // Check that either the auto save setting is enabled or fallback to activating it by default on mobile + return ( + settingsState?.editor?.autoSaveChanges ?? app.mobile.isCurrentDevice() + ) +} + +const throttledFileDidChange = debounce<(tab: FileTab) => Promise | void>( + async (tab) => { + tab.tryAutoSave() + }, + 3000 // 3s delay for fileDidChange +) + export abstract class FileTab extends Tab { public isForeignFile = false public isSaving = false @@ -72,6 +90,11 @@ export abstract class FileTab extends Tab { await super.setup() } + async onDeactivate() { + await this.tryAutoSave() + + super.onDeactivate() + } get name() { return this.fileHandle.name @@ -105,6 +128,24 @@ export abstract class FileTab extends Tab { abstract setReadOnly(readonly: TReadOnlyMode): Promise | void + /** + * **Important:** This function needs to be called when appropriate by the tabs implementing this class + */ + fileDidChange() { + throttledFileDidChange(this) + } + + // Logic for auto-saving + async tryAutoSave() { + // File handle has no parent context -> Auto-saving would have undesirable consequences such as constant file downloads + if (this.fileHandleWithoutParentContext()) return + + // Check whether we should auto save and that the file has been changed + if ((await shouldAutoSave()) && this.isUnsaved) { + await this.save() + } + } + async save() { if (this.isSaving) return this.isSaving = true @@ -153,12 +194,21 @@ export abstract class FileTab extends Tab { if (!this.isForeignFile) this.parent.openedFiles.add(this.getPath()) } - protected async writeFile(value: BufferSource | Blob | string) { - // Current file handle is a virtual file without parent - if ( + /** + * Check whether the given file handle has no parent context -> Save by "Save As" or file download + * @param fileHandle + * @returns boolean + */ + protected fileHandleWithoutParentContext() { + return ( this.fileHandle instanceof VirtualFileHandle && !this.fileHandle.hasParentContext - ) { + ) + } + + protected async writeFile(value: BufferSource | Blob | string) { + // Current file handle is a virtual file without parent + if (this.fileHandleWithoutParentContext()) { // Download the file if the user is using a file system polyfill if (isUsingFileSystemPolyfill.value) { download( diff --git a/src/components/Windows/Settings/setupSettings.ts b/src/components/Windows/Settings/setupSettings.ts index 8bb16484c..bf24b7a27 100644 --- a/src/components/Windows/Settings/setupSettings.ts +++ b/src/components/Windows/Settings/setupSettings.ts @@ -20,6 +20,8 @@ import { FontSelection } from './Controls/FontSelection' import { LocaleManager } from '../../Locales/Manager' export async function setupSettings(settings: SettingsWindow) { + const app = await App.getApp() + settings.addControl( new ButtonToggle({ category: 'appearance', @@ -29,7 +31,7 @@ export async function setupSettings(settings: SettingsWindow) { options: ['auto', 'dark', 'light'], default: 'auto', onChange: () => { - App.getApp().then((app) => app.themeManager.updateTheme()) + app.themeManager.updateTheme() }, }) ) @@ -46,7 +48,7 @@ export async function setupSettings(settings: SettingsWindow) { }, default: 'bridge.default.dark', onChange: () => { - App.getApp().then((app) => app.themeManager.updateTheme()) + app.themeManager.updateTheme() }, }) ) @@ -63,7 +65,7 @@ export async function setupSettings(settings: SettingsWindow) { }, default: 'bridge.default.light', onChange: () => { - App.getApp().then((app) => app.themeManager.updateTheme()) + app.themeManager.updateTheme() }, }) ) @@ -82,7 +84,7 @@ export async function setupSettings(settings: SettingsWindow) { }, default: 'bridge.noSelection', onChange: () => { - App.getApp().then((app) => app.themeManager.updateTheme()) + app.themeManager.updateTheme() }, }) ) @@ -101,7 +103,7 @@ export async function setupSettings(settings: SettingsWindow) { }, default: 'bridge.noSelection', onChange: () => { - App.getApp().then((app) => app.themeManager.updateTheme()) + app.themeManager.updateTheme() }, }) ) @@ -143,7 +145,6 @@ export async function setupSettings(settings: SettingsWindow) { 'monospace', ], onChange: async (val) => { - const app = await App.getApp() app.projectManager.updateAllEditorOptions({ fontFamily: val, }) @@ -160,7 +161,6 @@ export async function setupSettings(settings: SettingsWindow) { default: '14px', options: ['8px', '10px', '12px', '14px', '16px', '18px', '20px'], onChange: async (val) => { - const app = await App.getApp() app.projectManager.updateAllEditorOptions({ fontSize: Number(val.replace('px', '')), }) @@ -205,7 +205,7 @@ export async function setupSettings(settings: SettingsWindow) { options: ['tiny', 'small', 'normal', 'large'], default: 'normal', onChange: () => { - App.getApp().then((app) => app.windowResize.dispatch()) + app.windowResize.dispatch() }, }) ) @@ -313,16 +313,7 @@ export async function setupSettings(settings: SettingsWindow) { default: 'rawText', }) ) - settings.addControl( - new Toggle({ - category: 'editor', - name: 'windows.settings.editor.bridgePredictions.name', - description: - 'windows.settings.editor.bridgePredictions.description', - key: 'bridgePredictions', - default: true, - }) - ) + settings.addControl( new Toggle({ category: 'editor', @@ -332,7 +323,6 @@ export async function setupSettings(settings: SettingsWindow) { key: 'bracketPairColorization', default: false, onChange: async (val) => { - const app = await App.getApp() app.projectManager.updateAllEditorOptions({ // @ts-expect-error The monaco team did not update the types yet 'bracketPairColorization.enabled': val, @@ -348,7 +338,6 @@ export async function setupSettings(settings: SettingsWindow) { key: 'wordWrap', default: false, onChange: async (val) => { - const app = await App.getApp() app.projectManager.updateAllEditorOptions({ wordWrap: val ? 'bounded' : 'off', }) @@ -364,7 +353,6 @@ export async function setupSettings(settings: SettingsWindow) { default: '80', options: ['40', '60', '80', '100', '120', '140', '160'], onChange: async (val) => { - const app = await App.getApp() app.projectManager.updateAllEditorOptions({ wordWrapColumn: Number(val), }) @@ -389,6 +377,27 @@ export async function setupSettings(settings: SettingsWindow) { default: false, }) ) + settings.addControl( + new Toggle({ + category: 'editor', + name: 'windows.settings.editor.autoSaveChanges.name', + description: 'windows.settings.editor.autoSaveChanges.description', + key: 'autoSaveChanges', + default: app.mobile.isCurrentDevice(), // Auto save should be on by default on mobile + }) + ) + + // Tree Editor specific settings + settings.addControl( + new Toggle({ + category: 'editor', + name: 'windows.settings.editor.bridgePredictions.name', + description: + 'windows.settings.editor.bridgePredictions.description', + key: 'bridgePredictions', + default: true, + }) + ) settings.addControl( new Toggle({ category: 'editor', @@ -471,7 +480,6 @@ export async function setupSettings(settings: SettingsWindow) { ) // Actions - const app = await App.getApp() Object.values(app.actionManager.state).forEach((action) => { if (action.type === 'action') settings.addControl(new ActionViewer(action)) diff --git a/src/locales/en.json b/src/locales/en.json index adc136508..ca030f608 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -876,10 +876,7 @@ "name": "JSON Editor", "description": "Choose how you want to edit JSON files" }, - "bridgePredictions": { - "name": "bridge. Predictions", - "description": "Enable bridge. predictions to let the app intelligently decide whether to add a value or object within bridge.'s tree editor. This simplifies editing JSON significantly" - }, + "bracketPairColorization": { "name": "Bracket Pair Colorization", "description": "Give matching brackets an unique color" @@ -900,6 +897,14 @@ "name": "Keep Tabs Open", "description": "By default, opening a new tab closes the previously opened tab if it was not interacted with" }, + "autoSaveChanges": { + "name": "Auto Save Changes", + "description": "Automatically save changes to files after a short delay" + }, + "bridgePredictions": { + "name": "bridge. Predictions", + "description": "Enable bridge. predictions to let the app intelligently decide whether to add a value or object within bridge.'s tree editor. This simplifies editing JSON significantly" + }, "automaticallyOpenTreeNodes": { "name": "Automatically Open Tree Nodes", "description": "Inside of bridge.'s tree editor, automatically open nodes when you select them"