diff --git a/src/vs/platform/files/browser/htmlFileSystemProvider.ts b/src/vs/platform/files/browser/htmlFileSystemProvider.ts index 7ee315023b7..641036c1a7f 100644 --- a/src/vs/platform/files/browser/htmlFileSystemProvider.ts +++ b/src/vs/platform/files/browser/htmlFileSystemProvider.ts @@ -16,6 +16,8 @@ import { extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; import { createFileSystemProviderError, FileDeleteOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { IndexedDB } from 'vs/base/browser/indexedDB'; +import { ILogService } from 'vs/platform/log/common/log'; export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability { @@ -47,6 +49,13 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr //#endregion + + constructor( + private indexedDB: IndexedDB | undefined, + private readonly store: string, + private logService: ILogService + ) { } + //#region File Metadata Resolving async stat(resource: URI): Promise { @@ -289,11 +298,11 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr private readonly _files = new Map(); private readonly _directories = new Map(); - registerFileHandle(handle: FileSystemFileHandle): URI { + registerFileHandle(handle: FileSystemFileHandle): Promise { return this.registerHandle(handle, this._files); } - registerDirectoryHandle(handle: FileSystemDirectoryHandle): URI { + registerDirectoryHandle(handle: FileSystemDirectoryHandle): Promise { return this.registerHandle(handle, this._directories); } @@ -301,7 +310,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr return this._directories.values(); } - private registerHandle(handle: FileSystemHandle, map: Map): URI { + private async registerHandle(handle: FileSystemHandle, map: Map): Promise { let handleId = `/${handle.name}`; // Compute a valid handle ID in case this exists already @@ -314,13 +323,20 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr map.set(handleId, handle); + // Remember in IndexDB for future lookup + try { + await this.indexedDB?.runInTransaction(this.store, 'readwrite', objectStore => objectStore.put(handle, handleId)); + } catch (error) { + this.logService.error(error); + } + return URI.from({ scheme: Schemas.file, path: handleId }); } async getHandle(resource: URI): Promise { // First: try to find a well known handle first - let handle = this.getHandleSync(resource); + let handle = await this.doGetHandle(resource); // Second: walk up parent directories and resolve handle if possible if (!handle) { @@ -342,26 +358,8 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr return handle; } - private getHandleSync(resource: URI): FileSystemHandle | undefined { - - // We store file system handles with the `handle.name` - // and as such require the resource to be on the root - if (this.extUri.dirname(resource).path !== '/') { - return undefined; - } - - const handleId = resource.path.replace(/\/$/, ''); // remove potential slash from the end of the path - const handle = this._files.get(handleId) ?? this._directories.get(handleId); - - if (!handle) { - throw this.createFileSystemProviderError(resource, 'No file system handle registered', FileSystemProviderErrorCode.Unavailable); - } - - return handle; - } - private async getFileHandle(resource: URI): Promise { - const handle = this.getHandleSync(resource); + const handle = await this.doGetHandle(resource); if (handle instanceof FileSystemFileHandle) { return handle; } @@ -376,7 +374,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr } private async getDirectoryHandle(resource: URI): Promise { - const handle = this.getHandleSync(resource); + const handle = await this.doGetHandle(resource); if (handle instanceof FileSystemDirectoryHandle) { return handle; } @@ -390,6 +388,49 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr } } + private async doGetHandle(resource: URI): Promise { + + // We store file system handles with the `handle.name` + // and as such require the resource to be on the root + if (this.extUri.dirname(resource).path !== '/') { + return undefined; + } + + const handleId = resource.path.replace(/\/$/, ''); // remove potential slash from the end of the path + + // First: check if we have a known handle stored in memory + const inMemoryHandle = this._files.get(handleId) ?? this._directories.get(handleId); + if (inMemoryHandle) { + return inMemoryHandle; + } + + // Second: check if we have a persisted handle in IndexedDB + const persistedHandle = await this.indexedDB?.runInTransaction(this.store, 'readonly', store => store.get(handleId)); + if (WebFileSystemAccess.isFileSystemHandle(persistedHandle)) { + let hasPermissions = await persistedHandle.queryPermission() === 'granted'; + try { + if (!hasPermissions) { + hasPermissions = await persistedHandle.requestPermission() === 'granted'; + } + } catch (error) { + this.logService.error(error); // this can fail with a DOMException + } + + if (hasPermissions) { + if (WebFileSystemAccess.isFileSystemFileHandle(persistedHandle)) { + this._files.set(handleId, persistedHandle); + } else if (WebFileSystemAccess.isFileSystemDirectoryHandle(persistedHandle)) { + this._directories.set(handleId, persistedHandle); + } + + return persistedHandle; + } + } + + // Third: fail with an error + throw this.createFileSystemProviderError(resource, 'No file system handle registered', FileSystemProviderErrorCode.Unavailable); + } + //#endregion private toFileSystemProviderError(error: Error): FileSystemProviderError { diff --git a/src/vs/platform/workspace/common/virtualWorkspace.ts b/src/vs/platform/workspace/common/virtualWorkspace.ts index 4960b8eb356..77ccb98eaf2 100644 --- a/src/vs/platform/workspace/common/virtualWorkspace.ts +++ b/src/vs/platform/workspace/common/virtualWorkspace.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; -import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IWorkspace } from 'vs/platform/workspace/common/workspace'; @@ -28,11 +27,3 @@ export function getVirtualWorkspaceScheme(workspace: IWorkspace): string | undef export function isVirtualWorkspace(workspace: IWorkspace): boolean { return getVirtualWorkspaceLocation(workspace) !== undefined; } - -export function isTemporaryWorkspace(workspace: IWorkspace): boolean { - if (!isWeb) { - return false; // this concept only exists in web currently - } - - return workspace.configuration?.scheme === Schemas.tmp; -} diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 5b04e130c25..a0bd1b77d70 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -12,6 +12,7 @@ import { extname as resourceExtname, basenameOrAuthority, joinPath, extUriBiased import { URI, UriComponents } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Schemas } from 'vs/base/common/network'; export const IWorkspaceContextService = createDecorator('contextService'); @@ -418,6 +419,19 @@ export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentS return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome); } +export function isTemporaryWorkspace(workspace: IWorkspace): boolean; +export function isTemporaryWorkspace(path: URI): boolean; +export function isTemporaryWorkspace(arg1: IWorkspace | URI): boolean { + let path: URI | null | undefined; + if (URI.isUri(arg1)) { + path = arg1; + } else { + path = arg1.configuration; + } + + return path?.scheme === Schemas.tmp; +} + export function hasWorkspaceFileExtension(path: string | URI) { const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 5038f5e002c..bd5dadb880e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -14,7 +14,7 @@ import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEdit import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment } from 'vs/workbench/services/layout/browser/layoutService'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; @@ -45,7 +45,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ILogService } from 'vs/platform/log/common/log'; import { DeferredPromise, Promises } from 'vs/base/common/async'; import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; -import { isTemporaryWorkspace } from 'vs/platform/workspace/common/virtualWorkspace'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart'; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index ba4776a3a6b..5cfebbd665b 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -298,8 +298,9 @@ export class BrowserMain extends Disposable { let indexedDB: IndexedDB | undefined; const userDataStore = 'vscode-userdata-store'; const logsStore = 'vscode-logs-store'; + const handlesStore = 'vscode-filehandles-store'; try { - indexedDB = await IndexedDB.create('vscode-web-db', 2, [userDataStore, logsStore]); + indexedDB = await IndexedDB.create('vscode-web-db', 3, [userDataStore, logsStore, handlesStore]); // Close onWillShutdown this.onWillShutdownDisposables.add(toDisposable(() => indexedDB?.close())); @@ -336,7 +337,7 @@ export class BrowserMain extends Disposable { // Local file access (if supported by browser) if (WebFileSystemAccess.supported(window)) { - fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider()); + fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider(indexedDB, handlesStore, logService)); } // In-memory diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 02a494b146f..9726657c6bf 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -193,7 +193,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } } - private addFileToRecentlyOpened(uri: URI): void { + protected addFileToRecentlyOpened(uri: URI): void { // add the picked file into the list of recently opened // only if it is outside the currently opened workspace if (!this.contextService.isInsideWorkspace(uri)) { diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index 12efd7c86ec..eed8dff1901 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -35,7 +35,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } if (this.shouldUseSimplified(schema)) { - return this.pickFileFolderAndOpenSimplified(schema, options, false); + return super.pickFileFolderAndOpenSimplified(schema, options, false); } throw new Error(localize('pickFolderAndOpen', "Can't open folders, try adding a folder to the workspace instead.")); @@ -54,7 +54,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } if (this.shouldUseSimplified(schema)) { - return this.pickFileAndOpenSimplified(schema, options, false); + return super.pickFileAndOpenSimplified(schema, options, false); } if (!WebFileSystemAccess.supported(window)) { @@ -72,7 +72,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil return; } - const uri = this.fileSystemProvider.registerFileHandle(fileHandle); + const uri = await this.fileSystemProvider.registerFileHandle(fileHandle); + + this.addFileToRecentlyOpened(uri); await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); } @@ -85,7 +87,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } if (this.shouldUseSimplified(schema)) { - return this.pickFolderAndOpenSimplified(schema, options); + return super.pickFolderAndOpenSimplified(schema, options); } throw new Error(localize('pickFolderAndOpen', "Can't open folders, try adding a folder to the workspace instead.")); @@ -100,7 +102,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } if (this.shouldUseSimplified(schema)) { - return this.pickWorkspaceAndOpenSimplified(schema, options); + return super.pickWorkspaceAndOpenSimplified(schema, options); } throw new Error(localize('pickWorkspaceAndOpen', "Can't open workspaces, try adding a folder to the workspace instead.")); @@ -111,7 +113,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil const options = this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems); if (this.shouldUseSimplified(schema)) { - return this.pickFileToSaveSimplified(schema, options); + return super.pickFileToSaveSimplified(schema, options); } if (!WebFileSystemAccess.supported(window)) { @@ -152,7 +154,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil const schema = this.getFileSystemSchema(options); if (this.shouldUseSimplified(schema)) { - return this.showSaveDialogSimplified(schema, options); + return super.showSaveDialogSimplified(schema, options); } if (!WebFileSystemAccess.supported(window)) { @@ -179,7 +181,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil const schema = this.getFileSystemSchema(options); if (this.shouldUseSimplified(schema)) { - return this.showOpenDialogSimplified(schema, options); + return super.showOpenDialogSimplified(schema, options); } if (!WebFileSystemAccess.supported(window)) { @@ -193,11 +195,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil if (options.canSelectFiles) { const handle = await window.showOpenFilePicker({ multiple: false, types: this.getFilePickerTypes(options.filters), ...{ startIn } }); if (handle.length === 1 && WebFileSystemAccess.isFileSystemFileHandle(handle[0])) { - uri = this.fileSystemProvider.registerFileHandle(handle[0]); + uri = await this.fileSystemProvider.registerFileHandle(handle[0]); } } else { const handle = await window.showDirectoryPicker({ ...{ startIn } }); - uri = this.fileSystemProvider.registerDirectoryHandle(handle); + uri = await this.fileSystemProvider.registerDirectoryHandle(handle); } } catch (error) { // ignore - `showOpenFilePicker` / `showDirectoryPicker` will throw an error when the user cancels diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 32d1d654628..cdc63895e27 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -9,14 +9,13 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen } from 'vs/platform/window/common/window'; +import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen, IWorkspaceToOpen, IFolderToOpen } from 'vs/platform/window/common/window'; import { pathsToEditors } from 'vs/workbench/common/editor'; import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { ModifierKeyEmitter, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { memoize } from 'vs/base/common/decorators'; import { parseLineAndColumnAware } from 'vs/base/common/extpath'; @@ -32,6 +31,9 @@ import Severity from 'vs/base/common/severity'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DomEmitter } from 'vs/base/browser/event'; import { isUndefined } from 'vs/base/common/types'; +import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Schemas } from 'vs/base/common/network'; /** * A workspace to open in the workbench can either be: @@ -39,7 +41,7 @@ import { isUndefined } from 'vs/base/common/types'; * - a single folder (via `folderUri`) * - empty (via `undefined`) */ -export type IWorkspace = { workspaceUri: URI } | { folderUri: URI } | undefined; +export type IWorkspace = IWorkspaceToOpen | IFolderToOpen | undefined; export interface IWorkspaceProvider { @@ -108,7 +110,8 @@ export class BrowserHostService extends Disposable implements IHostService { @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleService private readonly lifecycleService: BrowserLifecycleService, @ILogService private readonly logService: ILogService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super(); @@ -239,16 +242,16 @@ export class BrowserHostService extends Disposable implements IHostService { // Handle Folders to Add if (foldersToAdd.length > 0) { - this.instantiationService.invokeFunction(accessor => { - const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService); // avoid heavy dependencies (https://github.com/microsoft/vscode/issues/108522) + this.withServices(accessor => { + const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService); workspaceEditingService.addFolders(foldersToAdd); }); } // Handle Files if (fileOpenables.length > 0) { - this.instantiationService.invokeFunction(async accessor => { - const editorService = accessor.get(IEditorService); // avoid heavy dependencies (https://github.com/microsoft/vscode/issues/108522) + this.withServices(async accessor => { + const editorService = accessor.get(IEditorService); // Support diffMode if (options?.diffMode && fileOpenables.length === 2) { @@ -328,6 +331,13 @@ export class BrowserHostService extends Disposable implements IHostService { } } + private withServices(fn: (accessor: ServicesAccessor) => unknown): void { + // Host service is used in a lot of contexts and some services + // need to be resolved dynamically to avoid cyclic dependencies + // (https://github.com/microsoft/vscode/issues/108522) + this.instantiationService.invokeFunction(accessor => fn(accessor)); + } + private preservePayload(): Array | undefined { // Selectively copy payload: for now only extension debugging properties are considered @@ -383,6 +393,20 @@ export class BrowserHostService extends Disposable implements IHostService { private async doOpen(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise { + // When we are in a temporary workspace and are asked to open a local folder + // we swap that folder into the workspace to avoid a window reload. Access + // to local resources is only possible without a window reload because it + // needs user activation. + if (workspace && isFolderToOpen(workspace) && workspace.folderUri.scheme === Schemas.file && isTemporaryWorkspace(this.contextService.getWorkspace())) { + this.withServices(async accessor => { + const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService); + + await workspaceEditingService.updateFolders(0, this.contextService.getWorkspace().folders.length, [{ uri: workspace.folderUri }]); + }); + + return; + } + // We know that `workspaceProvider.open` will trigger a shutdown // with `options.reuse` so we handle this expected shutdown if (options?.reuse) { diff --git a/src/vs/workbench/services/workspaces/browser/workspacesService.ts b/src/vs/workbench/services/workspaces/browser/workspacesService.ts index 9de46e5df04..64c1d2d19c9 100644 --- a/src/vs/workbench/services/workspaces/browser/workspacesService.ts +++ b/src/vs/workbench/services/workspaces/browser/workspacesService.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IWorkspacesService, IWorkspaceFolderCreationData, IEnterWorkspaceResult, IRecentlyOpened, restoreRecentlyOpened, IRecent, isRecentFile, isRecentFolder, toStoreData, IStoredWorkspaceFolder, getStoredWorkspaceFolder, IStoredWorkspace } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesService, IWorkspaceFolderCreationData, IEnterWorkspaceResult, IRecentlyOpened, restoreRecentlyOpened, IRecent, isRecentFile, isRecentFolder, toStoreData, IStoredWorkspaceFolder, getStoredWorkspaceFolder, IStoredWorkspace, isRecentWorkspace } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IWorkspaceContextService, IWorkspaceIdentifier, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; +import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { isTemporaryWorkspace, IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceIdentifier, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; import { ILogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; @@ -19,6 +19,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { isWindows } from 'vs/base/common/platform'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceBackupInfo, IFolderBackupInfo } from 'vs/platform/backup/common/backup'; +import { Schemas } from 'vs/base/common/network'; export class BrowserWorkspacesService extends Disposable implements IWorkspacesService { @@ -31,7 +32,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS constructor( @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @@ -47,17 +48,37 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS } private registerListeners(): void { - this._register(this.storageService.onDidChangeValue(event => { - if (event.key === BrowserWorkspacesService.RECENTLY_OPENED_KEY && event.scope === StorageScope.GLOBAL) { - this._onRecentlyOpenedChange.fire(); - } - })); + + // Storage + this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorage(e))); + + // Workspace + this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e))); + } + + private onDidChangeStorage(e: IStorageValueChangeEvent): void { + if (e.key === BrowserWorkspacesService.RECENTLY_OPENED_KEY && e.scope === StorageScope.GLOBAL) { + this._onRecentlyOpenedChange.fire(); + } + } + + private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void { + if (!isTemporaryWorkspace(this.contextService.getWorkspace())) { + return; + } + + // When in a temporary workspace, make sure to track folder changes + // in the history so that these can later be restored. + + for (const folder of e.added) { + this.addRecentlyOpened([{ folderUri: folder.uri }]); + } } private addWorkspaceToRecentlyOpened(): void { - const workspace = this.workspaceService.getWorkspace(); + const workspace = this.contextService.getWorkspace(); const remoteAuthority = this.environmentService.remoteAuthority; - switch (this.workspaceService.getWorkbenchState()) { + switch (this.contextService.getWorkbenchState()) { case WorkbenchState.FOLDER: this.addRecentlyOpened([{ folderUri: workspace.folders[0].uri, remoteAuthority }]); break; @@ -72,7 +93,26 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS async getRecentlyOpened(): Promise { const recentlyOpenedRaw = this.storageService.get(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL); if (recentlyOpenedRaw) { - return restoreRecentlyOpened(JSON.parse(recentlyOpenedRaw), this.logService); + const recentlyOpened = restoreRecentlyOpened(JSON.parse(recentlyOpenedRaw), this.logService); + recentlyOpened.workspaces = recentlyOpened.workspaces.filter(recent => { + + // In web, unless we are in a temporary workspace, we cannot support + // to switch to local folders because this would require a window + // reload and local file access only works with explicit user gesture + // from the current session. + if (isRecentFolder(recent) && recent.folderUri.scheme === Schemas.file && !isTemporaryWorkspace(this.contextService.getWorkspace())) { + return false; + } + + // Never offer temporary workspaces in the history + if (isRecentWorkspace(recent) && isTemporaryWorkspace(recent.workspace.configPath)) { + return false; + } + + return true; + }); + + return recentlyOpened; } return { workspaces: [], files: [] }; @@ -81,7 +121,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS async addRecentlyOpened(recents: IRecent[]): Promise { const recentlyOpened = await this.getRecentlyOpened(); - recents.forEach(recent => { + for (const recent of recents) { if (isRecentFile(recent)) { this.doRemoveRecentlyOpened(recentlyOpened, [recent.fileUri]); recentlyOpened.files.unshift(recent); @@ -92,7 +132,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS this.doRemoveRecentlyOpened(recentlyOpened, [recent.workspace.configPath]); recentlyOpened.workspaces.unshift(recent); } - }); + } return this.saveRecentlyOpened(recentlyOpened); }