mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 13:46:13 +00:00
web - allow to restore opened local files/folders via history (fix #135559)
This commit is contained in:
parent
ade0c657a4
commit
e77e14712c
|
@ -16,6 +16,8 @@ import { extUri, extUriIgnorePathCase } from 'vs/base/common/resources';
|
||||||
import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
|
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 { 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 { 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 {
|
export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability {
|
||||||
|
|
||||||
|
@ -47,6 +49,13 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private indexedDB: IndexedDB | undefined,
|
||||||
|
private readonly store: string,
|
||||||
|
private logService: ILogService
|
||||||
|
) { }
|
||||||
|
|
||||||
//#region File Metadata Resolving
|
//#region File Metadata Resolving
|
||||||
|
|
||||||
async stat(resource: URI): Promise<IStat> {
|
async stat(resource: URI): Promise<IStat> {
|
||||||
|
@ -289,11 +298,11 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
private readonly _files = new Map<string, FileSystemFileHandle>();
|
private readonly _files = new Map<string, FileSystemFileHandle>();
|
||||||
private readonly _directories = new Map<string, FileSystemDirectoryHandle>();
|
private readonly _directories = new Map<string, FileSystemDirectoryHandle>();
|
||||||
|
|
||||||
registerFileHandle(handle: FileSystemFileHandle): URI {
|
registerFileHandle(handle: FileSystemFileHandle): Promise<URI> {
|
||||||
return this.registerHandle(handle, this._files);
|
return this.registerHandle(handle, this._files);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerDirectoryHandle(handle: FileSystemDirectoryHandle): URI {
|
registerDirectoryHandle(handle: FileSystemDirectoryHandle): Promise<URI> {
|
||||||
return this.registerHandle(handle, this._directories);
|
return this.registerHandle(handle, this._directories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +310,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
return this._directories.values();
|
return this._directories.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandle(handle: FileSystemHandle, map: Map<string, FileSystemHandle>): URI {
|
private async registerHandle(handle: FileSystemHandle, map: Map<string, FileSystemHandle>): Promise<URI> {
|
||||||
let handleId = `/${handle.name}`;
|
let handleId = `/${handle.name}`;
|
||||||
|
|
||||||
// Compute a valid handle ID in case this exists already
|
// Compute a valid handle ID in case this exists already
|
||||||
|
@ -314,13 +323,20 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
|
|
||||||
map.set(handleId, handle);
|
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 });
|
return URI.from({ scheme: Schemas.file, path: handleId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHandle(resource: URI): Promise<FileSystemHandle | undefined> {
|
async getHandle(resource: URI): Promise<FileSystemHandle | undefined> {
|
||||||
|
|
||||||
// First: try to find a well known handle first
|
// 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
|
// Second: walk up parent directories and resolve handle if possible
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
|
@ -342,26 +358,8 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
return handle;
|
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<FileSystemFileHandle | undefined> {
|
private async getFileHandle(resource: URI): Promise<FileSystemFileHandle | undefined> {
|
||||||
const handle = this.getHandleSync(resource);
|
const handle = await this.doGetHandle(resource);
|
||||||
if (handle instanceof FileSystemFileHandle) {
|
if (handle instanceof FileSystemFileHandle) {
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
@ -376,7 +374,7 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDirectoryHandle(resource: URI): Promise<FileSystemDirectoryHandle | undefined> {
|
private async getDirectoryHandle(resource: URI): Promise<FileSystemDirectoryHandle | undefined> {
|
||||||
const handle = this.getHandleSync(resource);
|
const handle = await this.doGetHandle(resource);
|
||||||
if (handle instanceof FileSystemDirectoryHandle) {
|
if (handle instanceof FileSystemDirectoryHandle) {
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
@ -390,6 +388,49 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async doGetHandle(resource: URI): Promise<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
|
||||||
|
|
||||||
|
// 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
|
//#endregion
|
||||||
|
|
||||||
private toFileSystemProviderError(error: Error): FileSystemProviderError {
|
private toFileSystemProviderError(error: Error): FileSystemProviderError {
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { Schemas } from 'vs/base/common/network';
|
import { Schemas } from 'vs/base/common/network';
|
||||||
import { isWeb } from 'vs/base/common/platform';
|
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { IWorkspace } from 'vs/platform/workspace/common/workspace';
|
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 {
|
export function isVirtualWorkspace(workspace: IWorkspace): boolean {
|
||||||
return getVirtualWorkspaceLocation(workspace) !== undefined;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { extname as resourceExtname, basenameOrAuthority, joinPath, extUriBiased
|
||||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||||
|
import { Schemas } from 'vs/base/common/network';
|
||||||
|
|
||||||
export const IWorkspaceContextService = createDecorator<IWorkspaceContextService>('contextService');
|
export const IWorkspaceContextService = createDecorator<IWorkspaceContextService>('contextService');
|
||||||
|
|
||||||
|
@ -418,6 +419,19 @@ export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentS
|
||||||
return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome);
|
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) {
|
export function hasWorkspaceFileExtension(path: string | URI) {
|
||||||
const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path);
|
const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path);
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEdit
|
||||||
import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart';
|
import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart';
|
||||||
import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart';
|
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 { 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 { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
|
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 { ILogService } from 'vs/platform/log/common/log';
|
||||||
import { DeferredPromise, Promises } from 'vs/base/common/async';
|
import { DeferredPromise, Promises } from 'vs/base/common/async';
|
||||||
import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService';
|
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 { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
|
||||||
import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart';
|
import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart';
|
||||||
import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart';
|
import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart';
|
||||||
|
|
|
@ -298,8 +298,9 @@ export class BrowserMain extends Disposable {
|
||||||
let indexedDB: IndexedDB | undefined;
|
let indexedDB: IndexedDB | undefined;
|
||||||
const userDataStore = 'vscode-userdata-store';
|
const userDataStore = 'vscode-userdata-store';
|
||||||
const logsStore = 'vscode-logs-store';
|
const logsStore = 'vscode-logs-store';
|
||||||
|
const handlesStore = 'vscode-filehandles-store';
|
||||||
try {
|
try {
|
||||||
indexedDB = await IndexedDB.create('vscode-web-db', 2, [userDataStore, logsStore]);
|
indexedDB = await IndexedDB.create('vscode-web-db', 3, [userDataStore, logsStore, handlesStore]);
|
||||||
|
|
||||||
// Close onWillShutdown
|
// Close onWillShutdown
|
||||||
this.onWillShutdownDisposables.add(toDisposable(() => indexedDB?.close()));
|
this.onWillShutdownDisposables.add(toDisposable(() => indexedDB?.close()));
|
||||||
|
@ -336,7 +337,7 @@ export class BrowserMain extends Disposable {
|
||||||
|
|
||||||
// Local file access (if supported by browser)
|
// Local file access (if supported by browser)
|
||||||
if (WebFileSystemAccess.supported(window)) {
|
if (WebFileSystemAccess.supported(window)) {
|
||||||
fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider());
|
fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider(indexedDB, handlesStore, logService));
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory
|
// In-memory
|
||||||
|
|
|
@ -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
|
// add the picked file into the list of recently opened
|
||||||
// only if it is outside the currently opened workspace
|
// only if it is outside the currently opened workspace
|
||||||
if (!this.contextService.isInsideWorkspace(uri)) {
|
if (!this.contextService.isInsideWorkspace(uri)) {
|
||||||
|
|
|
@ -35,7 +35,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldUseSimplified(schema)) {
|
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."));
|
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)) {
|
if (this.shouldUseSimplified(schema)) {
|
||||||
return this.pickFileAndOpenSimplified(schema, options, false);
|
return super.pickFileAndOpenSimplified(schema, options, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WebFileSystemAccess.supported(window)) {
|
if (!WebFileSystemAccess.supported(window)) {
|
||||||
|
@ -72,7 +72,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
|
||||||
return;
|
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 } });
|
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)) {
|
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."));
|
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)) {
|
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."));
|
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);
|
const options = this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems);
|
||||||
if (this.shouldUseSimplified(schema)) {
|
if (this.shouldUseSimplified(schema)) {
|
||||||
return this.pickFileToSaveSimplified(schema, options);
|
return super.pickFileToSaveSimplified(schema, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WebFileSystemAccess.supported(window)) {
|
if (!WebFileSystemAccess.supported(window)) {
|
||||||
|
@ -152,7 +154,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
|
||||||
const schema = this.getFileSystemSchema(options);
|
const schema = this.getFileSystemSchema(options);
|
||||||
|
|
||||||
if (this.shouldUseSimplified(schema)) {
|
if (this.shouldUseSimplified(schema)) {
|
||||||
return this.showSaveDialogSimplified(schema, options);
|
return super.showSaveDialogSimplified(schema, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WebFileSystemAccess.supported(window)) {
|
if (!WebFileSystemAccess.supported(window)) {
|
||||||
|
@ -179,7 +181,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
|
||||||
const schema = this.getFileSystemSchema(options);
|
const schema = this.getFileSystemSchema(options);
|
||||||
|
|
||||||
if (this.shouldUseSimplified(schema)) {
|
if (this.shouldUseSimplified(schema)) {
|
||||||
return this.showOpenDialogSimplified(schema, options);
|
return super.showOpenDialogSimplified(schema, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WebFileSystemAccess.supported(window)) {
|
if (!WebFileSystemAccess.supported(window)) {
|
||||||
|
@ -193,11 +195,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
|
||||||
if (options.canSelectFiles) {
|
if (options.canSelectFiles) {
|
||||||
const handle = await window.showOpenFilePicker({ multiple: false, types: this.getFilePickerTypes(options.filters), ...{ startIn } });
|
const handle = await window.showOpenFilePicker({ multiple: false, types: this.getFilePickerTypes(options.filters), ...{ startIn } });
|
||||||
if (handle.length === 1 && WebFileSystemAccess.isFileSystemFileHandle(handle[0])) {
|
if (handle.length === 1 && WebFileSystemAccess.isFileSystemFileHandle(handle[0])) {
|
||||||
uri = this.fileSystemProvider.registerFileHandle(handle[0]);
|
uri = await this.fileSystemProvider.registerFileHandle(handle[0]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const handle = await window.showDirectoryPicker({ ...{ startIn } });
|
const handle = await window.showDirectoryPicker({ ...{ startIn } });
|
||||||
uri = this.fileSystemProvider.registerDirectoryHandle(handle);
|
uri = await this.fileSystemProvider.registerDirectoryHandle(handle);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore - `showOpenFilePicker` / `showDirectoryPicker` will throw an error when the user cancels
|
// ignore - `showOpenFilePicker` / `showDirectoryPicker` will throw an error when the user cancels
|
||||||
|
|
|
@ -9,14 +9,13 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
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 { pathsToEditors } from 'vs/workbench/common/editor';
|
||||||
import { whenEditorClosed } from 'vs/workbench/browser/editor';
|
import { whenEditorClosed } from 'vs/workbench/browser/editor';
|
||||||
import { IFileService } from 'vs/platform/files/common/files';
|
import { IFileService } from 'vs/platform/files/common/files';
|
||||||
import { ILabelService } from 'vs/platform/label/common/label';
|
import { ILabelService } from 'vs/platform/label/common/label';
|
||||||
import { ModifierKeyEmitter, trackFocus } from 'vs/base/browser/dom';
|
import { ModifierKeyEmitter, trackFocus } from 'vs/base/browser/dom';
|
||||||
import { Disposable } from 'vs/base/common/lifecycle';
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { URI } from 'vs/base/common/uri';
|
|
||||||
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
|
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
|
||||||
import { memoize } from 'vs/base/common/decorators';
|
import { memoize } from 'vs/base/common/decorators';
|
||||||
import { parseLineAndColumnAware } from 'vs/base/common/extpath';
|
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 { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||||
import { DomEmitter } from 'vs/base/browser/event';
|
import { DomEmitter } from 'vs/base/browser/event';
|
||||||
import { isUndefined } from 'vs/base/common/types';
|
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:
|
* 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`)
|
* - a single folder (via `folderUri`)
|
||||||
* - empty (via `undefined`)
|
* - empty (via `undefined`)
|
||||||
*/
|
*/
|
||||||
export type IWorkspace = { workspaceUri: URI } | { folderUri: URI } | undefined;
|
export type IWorkspace = IWorkspaceToOpen | IFolderToOpen | undefined;
|
||||||
|
|
||||||
export interface IWorkspaceProvider {
|
export interface IWorkspaceProvider {
|
||||||
|
|
||||||
|
@ -108,7 +110,8 @@ export class BrowserHostService extends Disposable implements IHostService {
|
||||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
@ILifecycleService private readonly lifecycleService: BrowserLifecycleService,
|
@ILifecycleService private readonly lifecycleService: BrowserLifecycleService,
|
||||||
@ILogService private readonly logService: ILogService,
|
@ILogService private readonly logService: ILogService,
|
||||||
@IDialogService private readonly dialogService: IDialogService
|
@IDialogService private readonly dialogService: IDialogService,
|
||||||
|
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -239,16 +242,16 @@ export class BrowserHostService extends Disposable implements IHostService {
|
||||||
|
|
||||||
// Handle Folders to Add
|
// Handle Folders to Add
|
||||||
if (foldersToAdd.length > 0) {
|
if (foldersToAdd.length > 0) {
|
||||||
this.instantiationService.invokeFunction(accessor => {
|
this.withServices(accessor => {
|
||||||
const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService); // avoid heavy dependencies (https://github.com/microsoft/vscode/issues/108522)
|
const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService);
|
||||||
workspaceEditingService.addFolders(foldersToAdd);
|
workspaceEditingService.addFolders(foldersToAdd);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Files
|
// Handle Files
|
||||||
if (fileOpenables.length > 0) {
|
if (fileOpenables.length > 0) {
|
||||||
this.instantiationService.invokeFunction(async accessor => {
|
this.withServices(async accessor => {
|
||||||
const editorService = accessor.get(IEditorService); // avoid heavy dependencies (https://github.com/microsoft/vscode/issues/108522)
|
const editorService = accessor.get(IEditorService);
|
||||||
|
|
||||||
// Support diffMode
|
// Support diffMode
|
||||||
if (options?.diffMode && fileOpenables.length === 2) {
|
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<unknown> | undefined {
|
private preservePayload(): Array<unknown> | undefined {
|
||||||
|
|
||||||
// Selectively copy payload: for now only extension debugging properties are considered
|
// 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<void> {
|
private async doOpen(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise<void> {
|
||||||
|
|
||||||
|
// 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
|
// We know that `workspaceProvider.open` will trigger a shutdown
|
||||||
// with `options.reuse` so we handle this expected shutdown
|
// with `options.reuse` so we handle this expected shutdown
|
||||||
if (options?.reuse) {
|
if (options?.reuse) {
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
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 { URI } from 'vs/base/common/uri';
|
||||||
import { Emitter } from 'vs/base/common/event';
|
import { Emitter } from 'vs/base/common/event';
|
||||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||||
import { IWorkspaceContextService, IWorkspaceIdentifier, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace';
|
import { isTemporaryWorkspace, IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceIdentifier, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace';
|
||||||
import { ILogService } from 'vs/platform/log/common/log';
|
import { ILogService } from 'vs/platform/log/common/log';
|
||||||
import { Disposable } from 'vs/base/common/lifecycle';
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces';
|
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 { isWindows } from 'vs/base/common/platform';
|
||||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||||
import { IWorkspaceBackupInfo, IFolderBackupInfo } from 'vs/platform/backup/common/backup';
|
import { IWorkspaceBackupInfo, IFolderBackupInfo } from 'vs/platform/backup/common/backup';
|
||||||
|
import { Schemas } from 'vs/base/common/network';
|
||||||
|
|
||||||
export class BrowserWorkspacesService extends Disposable implements IWorkspacesService {
|
export class BrowserWorkspacesService extends Disposable implements IWorkspacesService {
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@IStorageService private readonly storageService: IStorageService,
|
@IStorageService private readonly storageService: IStorageService,
|
||||||
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
|
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||||
@ILogService private readonly logService: ILogService,
|
@ILogService private readonly logService: ILogService,
|
||||||
@IFileService private readonly fileService: IFileService,
|
@IFileService private readonly fileService: IFileService,
|
||||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||||
|
@ -47,17 +48,37 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerListeners(): void {
|
private registerListeners(): void {
|
||||||
this._register(this.storageService.onDidChangeValue(event => {
|
|
||||||
if (event.key === BrowserWorkspacesService.RECENTLY_OPENED_KEY && event.scope === StorageScope.GLOBAL) {
|
// Storage
|
||||||
this._onRecentlyOpenedChange.fire();
|
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 {
|
private addWorkspaceToRecentlyOpened(): void {
|
||||||
const workspace = this.workspaceService.getWorkspace();
|
const workspace = this.contextService.getWorkspace();
|
||||||
const remoteAuthority = this.environmentService.remoteAuthority;
|
const remoteAuthority = this.environmentService.remoteAuthority;
|
||||||
switch (this.workspaceService.getWorkbenchState()) {
|
switch (this.contextService.getWorkbenchState()) {
|
||||||
case WorkbenchState.FOLDER:
|
case WorkbenchState.FOLDER:
|
||||||
this.addRecentlyOpened([{ folderUri: workspace.folders[0].uri, remoteAuthority }]);
|
this.addRecentlyOpened([{ folderUri: workspace.folders[0].uri, remoteAuthority }]);
|
||||||
break;
|
break;
|
||||||
|
@ -72,7 +93,26 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS
|
||||||
async getRecentlyOpened(): Promise<IRecentlyOpened> {
|
async getRecentlyOpened(): Promise<IRecentlyOpened> {
|
||||||
const recentlyOpenedRaw = this.storageService.get(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL);
|
const recentlyOpenedRaw = this.storageService.get(BrowserWorkspacesService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL);
|
||||||
if (recentlyOpenedRaw) {
|
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: [] };
|
return { workspaces: [], files: [] };
|
||||||
|
@ -81,7 +121,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS
|
||||||
async addRecentlyOpened(recents: IRecent[]): Promise<void> {
|
async addRecentlyOpened(recents: IRecent[]): Promise<void> {
|
||||||
const recentlyOpened = await this.getRecentlyOpened();
|
const recentlyOpened = await this.getRecentlyOpened();
|
||||||
|
|
||||||
recents.forEach(recent => {
|
for (const recent of recents) {
|
||||||
if (isRecentFile(recent)) {
|
if (isRecentFile(recent)) {
|
||||||
this.doRemoveRecentlyOpened(recentlyOpened, [recent.fileUri]);
|
this.doRemoveRecentlyOpened(recentlyOpened, [recent.fileUri]);
|
||||||
recentlyOpened.files.unshift(recent);
|
recentlyOpened.files.unshift(recent);
|
||||||
|
@ -92,7 +132,7 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS
|
||||||
this.doRemoveRecentlyOpened(recentlyOpened, [recent.workspace.configPath]);
|
this.doRemoveRecentlyOpened(recentlyOpened, [recent.workspace.configPath]);
|
||||||
recentlyOpened.workspaces.unshift(recent);
|
recentlyOpened.workspaces.unshift(recent);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return this.saveRecentlyOpened(recentlyOpened);
|
return this.saveRecentlyOpened(recentlyOpened);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue