未验证 提交 f7dde7f9 编写于 作者: B Benjamin Pasero 提交者: GitHub

Indicate dirty workspaces in recently opened (#93723)

* do not restore folders/workspaces with backups

* add method to getDirtyWorkspaces

* 💄 getRecentlyOpened

* identify dirty workspaces

* show a dialog

* change to one method

* fix actions

* 💄
上级 95343b6c
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
......@@ -15,6 +15,12 @@ export interface IWorkspaceBackupInfo {
remoteAuthority?: string;
}
export function isWorkspaceBackupInfo(obj: unknown): obj is IWorkspaceBackupInfo {
const candidate = obj as IWorkspaceBackupInfo;
return candidate && isWorkspaceIdentifier(candidate.workspace);
}
export interface IBackupMainService {
_serviceBrand: undefined;
......@@ -31,4 +37,12 @@ export interface IBackupMainService {
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
unregisterFolderBackupSync(folderUri: URI): void;
unregisterEmptyWindowBackupSync(backupFolder: string): void;
/**
* All folders or workspaces that are known to have
* backups stored. This call is long running because
* it checks for each backup location if any backups
* are stored.
*/
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
}
......@@ -9,7 +9,7 @@ import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs';
import * as arrays from 'vs/base/common/arrays';
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
......@@ -255,7 +255,7 @@ export class BackupMainService implements IBackupMainService {
seenIds.add(workspace.id);
const backupPath = this.getBackupPath(workspace.id);
const hasBackups = await this.hasBackups(backupPath);
const hasBackups = await this.doHasBackups(backupPath);
// If the workspace has no backups, ignore it
if (hasBackups) {
......@@ -287,7 +287,7 @@ export class BackupMainService implements IBackupMainService {
seenIds.add(key);
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
const hasBackups = await this.hasBackups(backupPath);
const hasBackups = await this.doHasBackups(backupPath);
// If the folder has no backups, ignore it
if (hasBackups) {
......@@ -325,7 +325,7 @@ export class BackupMainService implements IBackupMainService {
seenIds.add(backupFolder);
const backupPath = this.getBackupPath(backupFolder);
if (await this.hasBackups(backupPath)) {
if (await this.doHasBackups(backupPath)) {
result.push(backupInfo);
} else {
await this.deleteStaleBackup(backupPath);
......@@ -388,7 +388,48 @@ export class BackupMainService implements IBackupMainService {
return true;
}
private async hasBackups(backupPath: string): Promise<boolean> {
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
const dirtyWorkspaces: Array<IWorkspaceIdentifier | URI> = [];
// Workspaces with backups
for (const workspace of this.rootWorkspaces) {
if ((await this.hasBackups(workspace))) {
dirtyWorkspaces.push(workspace.workspace);
}
}
// Folders with backups
for (const folder of this.folderWorkspaces) {
if ((await this.hasBackups(folder))) {
dirtyWorkspaces.push(folder);
}
}
return dirtyWorkspaces;
}
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise<boolean> {
let backupPath: string;
// Folder
if (URI.isUri(backupLocation)) {
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
}
// Workspace
else if (isWorkspaceBackupInfo(backupLocation)) {
backupPath = this.getBackupPath(backupLocation.workspace.id);
}
// Empty
else {
backupPath = backupLocation.backupFolder;
}
return this.doHasBackups(backupPath);
}
private async doHasBackups(backupPath: string): Promise<boolean> {
try {
const backupSchemas = await readdir(backupPath);
......
......@@ -22,6 +22,7 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { createHash } from 'crypto';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
suite('BackupMainService', () => {
......@@ -731,4 +732,45 @@ suite('BackupMainService', () => {
}
});
});
suite('getDirtyWorkspaces', () => {
test('should report if a workspace or folder has backups', async () => {
const folderBackupPath = service.registerFolderBackupSync(fooFile);
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
try {
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled));
} catch (error) {
// ignore - folder might exist already
}
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), '');
fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), '');
const dirtyWorkspaces = await service.getDirtyWorkspaces();
assert.equal(dirtyWorkspaces.length, 2);
let found = 0;
for (const dirtyWorkpspace of dirtyWorkspaces) {
if (URI.isUri(dirtyWorkpspace)) {
if (isEqual(fooFile, dirtyWorkpspace)) {
found++;
}
} else {
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) {
found++;
}
}
}
assert.equal(found, 2);
});
});
});
......@@ -452,14 +452,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
}
//
// These are windows to restore because of hot-exit or from previous session that would otherwise be lost (only performed once on startup!)
// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
//
let workspacesToRestore: IWorkspacePathToOpen[] = [];
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) {
// collect from workspaces with hot-exit backups and from previous window session
// Untitled workspaces are always restored
workspacesToRestore = this.workspacesMainService.getUntitledWorkspacesSync();
workspacesToOpen.push(...workspacesToRestore);
// Empty windows with backups are always restored
emptyToRestore.push(...this.backupMainService.getEmptyWindowBackupPaths());
} else {
emptyToRestore.length = 0;
......
......@@ -18,7 +18,7 @@ import { toSlashes } from 'vs/base/common/extpath';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
import { ILogService } from 'vs/platform/log/common/log';
import { Event as CommonEvent } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export const WORKSPACE_EXTENSION = 'code-workspace';
......@@ -31,18 +31,21 @@ export interface IWorkspacesService {
_serviceBrand: undefined;
// Management
// Workspaces Management
enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | null>;
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;
// History
readonly onRecentlyOpenedChange: CommonEvent<void>;
// Workspaces History
readonly onRecentlyOpenedChange: Event<void>;
addRecentlyOpened(recents: IRecent[]): Promise<void>;
removeRecentlyOpened(workspaces: URI[]): Promise<void>;
clearRecentlyOpened(): Promise<void>;
getRecentlyOpened(): Promise<IRecentlyOpened>;
// Dirty Workspaces
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
}
export interface IRecentlyOpened {
......
......@@ -9,7 +9,6 @@ import { IStateService } from 'vs/platform/state/node/state';
import { app, JumpListCategory } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels';
import { IPath } from 'vs/platform/windows/common/windows';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/workspaces/common/workspaces';
......@@ -24,6 +23,7 @@ import { exists } from 'vs/base/node/pfs';
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Disposable } from 'vs/base/common/lifecycle';
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
export const IWorkspacesHistoryMainService = createDecorator<IWorkspacesHistoryMainService>('workspacesHistoryMainService');
......@@ -34,7 +34,7 @@ export interface IWorkspacesHistoryMainService {
readonly onRecentlyOpenedChange: CommonEvent<void>;
addRecentlyOpened(recents: IRecent[]): void;
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened;
removeRecentlyOpened(paths: URI[]): void;
clearRecentlyOpened(): void;
......@@ -241,20 +241,23 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
this._onRecentlyOpenedChange.fire();
}
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened {
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
const files: IRecentFile[] = [];
// Add current workspace to beginning if set
const currentWorkspace = include?.config?.workspace;
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
workspaces.push({ workspace: currentWorkspace });
}
const currentFolder = include?.config?.folderUri;
if (currentFolder) {
workspaces.push({ folderUri: currentFolder });
}
// Add currently files to open to the beginning if any
const currentFiles = include?.config?.filesToOpenOrCreate;
if (currentFiles) {
for (let currentFile of currentFiles) {
const fileUri = currentFile.fileUri;
......
......@@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspacesService, Promise<unknown> /* only methods, not events */, number /* window ID */> {
......@@ -17,7 +18,8 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
constructor(
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
@IBackupMainService private readonly backupMainService: IBackupMainService
) {
}
......@@ -51,12 +53,7 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange;
async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> {
const window = this.windowsMainService.getWindowById(windowId);
if (window?.config) {
return this.workspacesHistoryMainService.getRecentlyOpened(window.config.workspace, window.config.folderUri, window.config.filesToOpenOrCreate);
}
return this.workspacesHistoryMainService.getRecentlyOpened();
return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId));
}
async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise<void> {
......@@ -72,4 +69,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
}
//#endregion
//#region Dirty Workspaces
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
return this.backupMainService.getDirtyWorkspaces();
}
//#endregion
}
......@@ -56,6 +56,7 @@ export class TestDialogMainService implements IDialogMainService {
}
export class TestBackupMainService implements IBackupMainService {
_serviceBrand: undefined;
isHotExitEnabled(): boolean {
......@@ -97,6 +98,10 @@ export class TestBackupMainService implements IBackupMainService {
unregisterEmptyWindowBackupSync(backupFolder: string): void {
throw new Error('Method not implemented.');
}
async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> {
return [];
}
}
suite('WorkspacesMainService', () => {
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/screencast';
import 'vs/css!./media/actions';
import { Action } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
......
......@@ -3,6 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before {
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty workspaces */
}
.monaco-workbench .screencast-mouse {
position: absolute;
border: 2px solid red;
......
......@@ -14,13 +14,13 @@ import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys';
import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILabelService } from 'vs/platform/label/common/label';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IRecentWorkspace, IRecentFolder, IRecentFile, IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { IRecent, isRecentFolder, isRecentWorkspace, IWorkspacesService, IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { FileKind } from 'vs/platform/files/common/files';
......@@ -29,9 +29,15 @@ import { isMacintosh } from 'vs/base/common/platform';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { inQuickPickContext, getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { ResourceMap } from 'vs/base/common/map';
export const inRecentFilesPickerContextKey = 'inRecentFilesPicker';
interface IRecentlyOpenedPick extends IQuickPickItem {
resource: URI,
openable: IWindowOpenable;
}
abstract class BaseOpenRecentAction extends Action {
private readonly removeFromRecentlyOpened: IQuickInputButton = {
......@@ -39,6 +45,12 @@ abstract class BaseOpenRecentAction extends Action {
tooltip: nls.localize('remove', "Remove from Recently Opened")
};
private readonly dirtyRecentlyOpened: IQuickInputButton = {
iconClass: 'dirty-workspace codicon-circle-filled',
tooltip: nls.localize('dirtyRecentlyOpened', "Workspace With Dirty Files"),
alwaysVisible: true
};
constructor(
id: string,
label: string,
......@@ -49,7 +61,8 @@ abstract class BaseOpenRecentAction extends Action {
private keybindingService: IKeybindingService,
private modelService: IModelService,
private modeService: IModeService,
private hostService: IHostService
private hostService: IHostService,
private dialogService: IDialogService
) {
super(id, label);
}
......@@ -57,61 +70,53 @@ abstract class BaseOpenRecentAction extends Action {
protected abstract isQuickNavigate(): boolean;
async run(): Promise<void> {
const { workspaces, files } = await this.workspacesService.getRecentlyOpened();
this.openRecent(workspaces, files);
}
private async openRecent(recentWorkspaces: Array<IRecentWorkspace | IRecentFolder>, recentFiles: IRecentFile[]): Promise<void> {
const toPick = (recent: IRecent, labelService: ILabelService, buttons: IQuickInputButton[] | undefined) => {
let openable: IWindowOpenable | undefined;
let iconClasses: string[];
let fullLabel: string | undefined;
let resource: URI | undefined;
// Folder
if (isRecentFolder(recent)) {
resource = recent.folderUri;
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER);
openable = { folderUri: resource };
fullLabel = recent.label || labelService.getWorkspaceLabel(resource, { verbose: true });
const recentlyOpened = await this.workspacesService.getRecentlyOpened();
const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces();
// Identify all folders and workspaces with dirty files
const dirtyFolders = new ResourceMap<boolean>();
const dirtyWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
for (const dirtyWorkspace of dirtyWorkspacesAndFolders) {
if (URI.isUri(dirtyWorkspace)) {
dirtyFolders.set(dirtyWorkspace, true);
} else {
dirtyWorkspaces.set(dirtyWorkspace.configPath, dirtyWorkspace);
}
}
// Workspace
else if (isRecentWorkspace(recent)) {
resource = recent.workspace.configPath;
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER);
openable = { workspaceUri: resource };
fullLabel = recent.label || labelService.getWorkspaceLabel(recent.workspace, { verbose: true });
// Identify all recently opened folders and workspaces
const recentFolders = new ResourceMap<boolean>();
const recentWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
for (const recent of recentlyOpened.workspaces) {
if (isRecentFolder(recent)) {
recentFolders.set(recent.folderUri, true);
} else {
recentWorkspaces.set(recent.workspace.configPath, recent.workspace);
}
}
// File
else {
resource = recent.fileUri;
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE);
openable = { fileUri: resource };
fullLabel = recent.label || labelService.getUriLabel(resource);
}
// Fill in all known recently opened workspaces
const workspacePicks: IRecentlyOpenedPick[] = [];
for (const recent of recentlyOpened.workspaces) {
const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath);
const { name, parentPath } = splitName(fullLabel);
workspacePicks.push(this.toQuickPick(recent, isDirty));
}
return {
iconClasses,
label: name,
description: parentPath,
buttons,
openable,
resource
};
};
// Fill any backup workspace that is not yet shown at the end
for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) {
if (URI.isUri(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder)) {
workspacePicks.push(this.toQuickPick({ folderUri: dirtyWorkspaceOrFolder }, true));
} else if (isWorkspaceIdentifier(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.configPath)) {
workspacePicks.push(this.toQuickPick({ workspace: dirtyWorkspaceOrFolder }, true));
}
}
const workspacePicks = recentWorkspaces.map(workspace => toPick(workspace, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined));
const filePicks = recentFiles.map(p => toPick(p, this.labelService, !this.isQuickNavigate() ? [this.removeFromRecentlyOpened] : undefined));
const filePicks = recentlyOpened.files.map(p => this.toQuickPick(p, false));
// focus second entry if the first recent workspace is the current workspace
const firstEntry = recentWorkspaces[0];
let autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri);
const firstEntry = recentlyOpened.workspaces[0];
const autoFocusSecondEntry: boolean = firstEntry && this.contextService.isCurrentWorkspace(isRecentWorkspace(firstEntry) ? firstEntry.workspace : firstEntry.folderUri);
let keyMods: IKeyMods | undefined;
......@@ -127,8 +132,27 @@ abstract class BaseOpenRecentAction extends Action {
onKeyMods: mods => keyMods = mods,
quickNavigate: this.isQuickNavigate() ? { keybindings: this.keybindingService.lookupKeybindings(this.id) } : undefined,
onDidTriggerItemButton: async context => {
await this.workspacesService.removeRecentlyOpened([context.item.resource]);
context.removeItem();
// Remove
if (context.button === this.removeFromRecentlyOpened) {
await this.workspacesService.removeRecentlyOpened([context.item.resource]);
context.removeItem();
}
// Dirty Workspace
else if (context.button === this.dirtyRecentlyOpened) {
const result = await this.dialogService.confirm({
type: 'question',
title: nls.localize('dirtyWorkspace', "Workspace with Dirty Files"),
message: nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the dirty files?"),
detail: nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with dirty files cannot be removed until all dirty files have been saved or reverted.")
});
if (result.confirmed) {
this.hostService.openWindow([context.item.openable]);
this.quickInputService.cancel();
}
}
}
});
......@@ -136,6 +160,48 @@ abstract class BaseOpenRecentAction extends Action {
return this.hostService.openWindow([pick.openable], { forceNewWindow: keyMods?.ctrlCmd, forceReuseWindow: keyMods?.alt });
}
}
private toQuickPick(recent: IRecent, isDirty: boolean): IRecentlyOpenedPick {
let openable: IWindowOpenable | undefined;
let iconClasses: string[];
let fullLabel: string | undefined;
let resource: URI | undefined;
// Folder
if (isRecentFolder(recent)) {
resource = recent.folderUri;
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FOLDER);
openable = { folderUri: resource };
fullLabel = recent.label || this.labelService.getWorkspaceLabel(resource, { verbose: true });
}
// Workspace
else if (isRecentWorkspace(recent)) {
resource = recent.workspace.configPath;
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER);
openable = { workspaceUri: resource };
fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true });
}
// File
else {
resource = recent.fileUri;
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.FILE);
openable = { fileUri: resource };
fullLabel = recent.label || this.labelService.getUriLabel(resource);
}
const { name, parentPath } = splitName(fullLabel);
return {
iconClasses,
label: name,
description: parentPath,
buttons: isDirty ? [this.dirtyRecentlyOpened] : [this.removeFromRecentlyOpened],
openable,
resource
};
}
}
export class OpenRecentAction extends BaseOpenRecentAction {
......@@ -153,9 +219,10 @@ export class OpenRecentAction extends BaseOpenRecentAction {
@IModelService modelService: IModelService,
@IModeService modeService: IModeService,
@ILabelService labelService: ILabelService,
@IHostService hostService: IHostService
@IHostService hostService: IHostService,
@IDialogService dialogService: IDialogService
) {
super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService);
super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService);
}
protected isQuickNavigate(): boolean {
......@@ -178,9 +245,10 @@ class QuickPickRecentAction extends BaseOpenRecentAction {
@IModelService modelService: IModelService,
@IModeService modeService: IModeService,
@ILabelService labelService: ILabelService,
@IHostService hostService: IHostService
@IHostService hostService: IHostService,
@IDialogService dialogService: IDialogService
) {
super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService);
super(id, label, workspacesService, quickInputService, contextService, labelService, keybindingService, modelService, modeService, hostService, dialogService);
}
protected isQuickNavigate(): boolean {
......
......@@ -4,5 +4,5 @@
*--------------------------------------------------------------------------------------------*/
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-window::before {
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty open editors */
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty windows */
}
......@@ -165,6 +165,15 @@ export class BrowserWorkspacesService extends Disposable implements IWorkspacesS
}
//#endregion
//#region Dirty Workspaces
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
return []; // Currently not supported in web
}
//#endregion
}
registerSingleton(IWorkspacesService, BrowserWorkspacesService, true);
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册