mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-12 11:08:31 -05:00
Merge from vscode e0762af258c0b20320ed03f3871a41967acc4421 (#7404)
* Merge from vscode e0762af258c0b20320ed03f3871a41967acc4421 * readd svgs
This commit is contained in:
@@ -17,7 +17,6 @@ import { normalizeDriveLetter } from 'vs/base/common/labels';
|
||||
import { toSlashes } from 'vs/base/common/extpath';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IEnterWorkspaceResult } from 'vs/platform/windows/common/windows';
|
||||
|
||||
export const WORKSPACE_EXTENSION = 'code-workspace';
|
||||
export const WORKSPACE_FILTER = [{ name: localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }];
|
||||
@@ -92,6 +91,11 @@ export interface IUntitledWorkspaceInfo {
|
||||
remoteAuthority?: string;
|
||||
}
|
||||
|
||||
export interface IEnterWorkspaceResult {
|
||||
workspace: IWorkspaceIdentifier;
|
||||
backupPath?: string;
|
||||
}
|
||||
|
||||
export const IWorkspacesService = createDecorator<IWorkspacesService>('workspacesService');
|
||||
|
||||
export interface IWorkspacesService {
|
||||
|
||||
41
src/vs/platform/workspaces/common/workspacesHistory.ts
Normal file
41
src/vs/platform/workspaces/common/workspacesHistory.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export interface IRecentlyOpened {
|
||||
workspaces: Array<IRecentWorkspace | IRecentFolder>;
|
||||
files: IRecentFile[];
|
||||
}
|
||||
|
||||
export type IRecent = IRecentWorkspace | IRecentFolder | IRecentFile;
|
||||
|
||||
export interface IRecentWorkspace {
|
||||
workspace: IWorkspaceIdentifier;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IRecentFolder {
|
||||
folderUri: ISingleFolderWorkspaceIdentifier;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IRecentFile {
|
||||
fileUri: URI;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function isRecentWorkspace(curr: IRecent): curr is IRecentWorkspace {
|
||||
return curr.hasOwnProperty('workspace');
|
||||
}
|
||||
|
||||
export function isRecentFolder(curr: IRecent): curr is IRecentFolder {
|
||||
return curr.hasOwnProperty('folderUri');
|
||||
}
|
||||
|
||||
export function isRecentFile(curr: IRecent): curr is IRecentFile {
|
||||
return curr.hasOwnProperty('fileUri');
|
||||
}
|
||||
129
src/vs/platform/workspaces/common/workspacesHistoryStorage.ts
Normal file
129
src/vs/platform/workspaces/common/workspacesHistoryStorage.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { UriComponents, URI } from 'vs/base/common/uri';
|
||||
import { IRecentlyOpened, isRecentFolder } from 'vs/platform/workspaces/common/workspacesHistory';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
interface ISerializedRecentlyOpened {
|
||||
workspaces3: Array<ISerializedWorkspace | string>; // workspace or URI.toString() // added in 1.32
|
||||
workspaceLabels?: Array<string | null>; // added in 1.33
|
||||
files2: string[]; // files as URI.toString() // added in 1.32
|
||||
fileLabels?: Array<string | null>; // added in 1.33
|
||||
}
|
||||
|
||||
interface ILegacySerializedRecentlyOpened {
|
||||
workspaces2: Array<ILegacySerializedWorkspace | string>; // legacy, configPath as file path
|
||||
workspaces: Array<ILegacySerializedWorkspace | string | UriComponents>; // legacy (UriComponents was also supported for a few insider builds)
|
||||
files: string[]; // files as paths
|
||||
}
|
||||
|
||||
interface ISerializedWorkspace { id: string; configURIPath: string; }
|
||||
interface ILegacySerializedWorkspace { id: string; configPath: string; }
|
||||
|
||||
function isLegacySerializedWorkspace(curr: any): curr is ILegacySerializedWorkspace {
|
||||
return typeof curr === 'object' && typeof curr['id'] === 'string' && typeof curr['configPath'] === 'string';
|
||||
}
|
||||
|
||||
function isUriComponents(curr: any): curr is UriComponents {
|
||||
return curr && typeof curr['path'] === 'string' && typeof curr['scheme'] === 'string';
|
||||
}
|
||||
|
||||
export type RecentlyOpenedStorageData = object;
|
||||
|
||||
export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefined, logService: ILogService): IRecentlyOpened {
|
||||
const result: IRecentlyOpened = { workspaces: [], files: [] };
|
||||
if (data) {
|
||||
const restoreGracefully = function <T>(entries: T[], func: (entry: T, index: number) => void) {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
try {
|
||||
func(entries[i], i);
|
||||
} catch (e) {
|
||||
logService.warn(`Error restoring recent entry ${JSON.stringify(entries[i])}: ${e.toString()}. Skip entry.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const storedRecents = data as ISerializedRecentlyOpened & ILegacySerializedRecentlyOpened;
|
||||
if (Array.isArray(storedRecents.workspaces3)) {
|
||||
restoreGracefully(storedRecents.workspaces3, (workspace, i) => {
|
||||
const label: string | undefined = (Array.isArray(storedRecents.workspaceLabels) && storedRecents.workspaceLabels[i]) || undefined;
|
||||
if (typeof workspace === 'object' && typeof workspace.id === 'string' && typeof workspace.configURIPath === 'string') {
|
||||
result.workspaces.push({ label, workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) } });
|
||||
} else if (typeof workspace === 'string') {
|
||||
result.workspaces.push({ label, folderUri: URI.parse(workspace) });
|
||||
}
|
||||
});
|
||||
} else if (Array.isArray(storedRecents.workspaces2)) {
|
||||
restoreGracefully(storedRecents.workspaces2, workspace => {
|
||||
if (typeof workspace === 'object' && typeof workspace.id === 'string' && typeof workspace.configPath === 'string') {
|
||||
result.workspaces.push({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } });
|
||||
} else if (typeof workspace === 'string') {
|
||||
result.workspaces.push({ folderUri: URI.parse(workspace) });
|
||||
}
|
||||
});
|
||||
} else if (Array.isArray(storedRecents.workspaces)) {
|
||||
// TODO@martin legacy support can be removed at some point (6 month?)
|
||||
// format of 1.25 and before
|
||||
restoreGracefully(storedRecents.workspaces, workspace => {
|
||||
if (typeof workspace === 'string') {
|
||||
result.workspaces.push({ folderUri: URI.file(workspace) });
|
||||
} else if (isLegacySerializedWorkspace(workspace)) {
|
||||
result.workspaces.push({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } });
|
||||
} else if (isUriComponents(workspace)) {
|
||||
// added by 1.26-insiders
|
||||
result.workspaces.push({ folderUri: URI.revive(<UriComponents>workspace) });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (Array.isArray(storedRecents.files2)) {
|
||||
restoreGracefully(storedRecents.files2, (file, i) => {
|
||||
const label: string | undefined = (Array.isArray(storedRecents.fileLabels) && storedRecents.fileLabels[i]) || undefined;
|
||||
if (typeof file === 'string') {
|
||||
result.files.push({ label, fileUri: URI.parse(file) });
|
||||
}
|
||||
});
|
||||
} else if (Array.isArray(storedRecents.files)) {
|
||||
restoreGracefully(storedRecents.files, file => {
|
||||
if (typeof file === 'string') {
|
||||
result.files.push({ fileUri: URI.file(file) });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function toStoreData(recents: IRecentlyOpened): RecentlyOpenedStorageData {
|
||||
const serialized: ISerializedRecentlyOpened = { workspaces3: [], files2: [] };
|
||||
|
||||
let hasLabel = false;
|
||||
const workspaceLabels: (string | null)[] = [];
|
||||
for (const recent of recents.workspaces) {
|
||||
if (isRecentFolder(recent)) {
|
||||
serialized.workspaces3.push(recent.folderUri.toString());
|
||||
} else {
|
||||
serialized.workspaces3.push({ id: recent.workspace.id, configURIPath: recent.workspace.configPath.toString() });
|
||||
}
|
||||
workspaceLabels.push(recent.label || null);
|
||||
hasLabel = hasLabel || !!recent.label;
|
||||
}
|
||||
if (hasLabel) {
|
||||
serialized.workspaceLabels = workspaceLabels;
|
||||
}
|
||||
|
||||
hasLabel = false;
|
||||
const fileLabels: (string | null)[] = [];
|
||||
for (const recent of recents.files) {
|
||||
serialized.files2.push(recent.fileUri.toString());
|
||||
fileLabels.push(recent.label || null);
|
||||
hasLabel = hasLabel || !!recent.label;
|
||||
}
|
||||
if (hasLabel) {
|
||||
serialized.fileLabels = fileLabels;
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { app, JumpListCategory } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getBaseLabel, getPathLabel } 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 } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile } from 'vs/platform/workspaces/common/workspacesHistory';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { isEqual as areResourcesEqual, dirname, originalFSPath, basename } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { getSimpleWorkspaceLabel } from 'vs/platform/label/common/label';
|
||||
import { toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/workspaces/common/workspacesHistoryStorage';
|
||||
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';
|
||||
|
||||
export const IWorkspacesHistoryMainService = createDecorator<IWorkspacesHistoryMainService>('workspacesHistoryMainService');
|
||||
|
||||
export interface IWorkspacesHistoryMainService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
onRecentlyOpenedChange: CommonEvent<void>;
|
||||
|
||||
addRecentlyOpened(recents: IRecent[]): void;
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
|
||||
removeFromRecentlyOpened(paths: URI[]): void;
|
||||
clearRecentlyOpened(): void;
|
||||
|
||||
updateWindowsJumpList(): void;
|
||||
}
|
||||
|
||||
export class WorkspacesHistoryMainService implements IWorkspacesHistoryMainService {
|
||||
|
||||
private static readonly MAX_TOTAL_RECENT_ENTRIES = 100;
|
||||
|
||||
private static readonly MAX_MACOS_DOCK_RECENT_WORKSPACES = 7; // prefer more workspaces...
|
||||
private static readonly MAX_MACOS_DOCK_RECENT_ENTRIES_TOTAL = 10; // ...compared to files
|
||||
|
||||
// Exclude some very common files from the dock/taskbar
|
||||
private static readonly COMMON_FILES_FILTER = [
|
||||
'COMMIT_EDITMSG',
|
||||
'MERGE_MSG'
|
||||
];
|
||||
|
||||
private static readonly recentlyOpenedStorageKey = 'openedPathsList';
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onRecentlyOpenedChange = new Emitter<void>();
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
|
||||
|
||||
private macOSRecentDocumentsUpdater: ThrottledDelayer<void>;
|
||||
|
||||
constructor(
|
||||
@IStateService private readonly stateService: IStateService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
|
||||
) {
|
||||
this.macOSRecentDocumentsUpdater = new ThrottledDelayer<void>(800);
|
||||
|
||||
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList());
|
||||
}
|
||||
|
||||
private handleWindowsJumpList(): void {
|
||||
if (!isWindows) {
|
||||
return; // only on windows
|
||||
}
|
||||
|
||||
this.updateWindowsJumpList();
|
||||
this.onRecentlyOpenedChange(() => this.updateWindowsJumpList());
|
||||
}
|
||||
|
||||
addRecentlyOpened(newlyAdded: IRecent[]): void {
|
||||
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
|
||||
const files: IRecentFile[] = [];
|
||||
|
||||
for (let curr of newlyAdded) {
|
||||
|
||||
// Workspace
|
||||
if (isRecentWorkspace(curr)) {
|
||||
if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) {
|
||||
workspaces.push(curr);
|
||||
}
|
||||
}
|
||||
|
||||
// Folder
|
||||
else if (isRecentFolder(curr)) {
|
||||
if (indexOfFolder(workspaces, curr.folderUri) === -1) {
|
||||
workspaces.push(curr);
|
||||
}
|
||||
}
|
||||
|
||||
// File
|
||||
else {
|
||||
const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0;
|
||||
const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0;
|
||||
|
||||
if (!alreadyExistsInHistory && !shouldBeFiltered) {
|
||||
files.push(curr);
|
||||
|
||||
// Add to recent documents (Windows only, macOS later)
|
||||
if (isWindows && curr.fileUri.scheme === Schemas.file) {
|
||||
app.addRecentDocument(curr.fileUri.fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addEntriesFromStorage(workspaces, files);
|
||||
|
||||
if (workspaces.length > WorkspacesHistoryMainService.MAX_TOTAL_RECENT_ENTRIES) {
|
||||
workspaces.length = WorkspacesHistoryMainService.MAX_TOTAL_RECENT_ENTRIES;
|
||||
}
|
||||
|
||||
if (files.length > WorkspacesHistoryMainService.MAX_TOTAL_RECENT_ENTRIES) {
|
||||
files.length = WorkspacesHistoryMainService.MAX_TOTAL_RECENT_ENTRIES;
|
||||
}
|
||||
|
||||
this.saveRecentlyOpened({ workspaces, files });
|
||||
this._onRecentlyOpenedChange.fire();
|
||||
|
||||
// Schedule update to recent documents on macOS dock
|
||||
if (isMacintosh) {
|
||||
this.macOSRecentDocumentsUpdater.trigger(() => this.updateMacOSRecentDocuments());
|
||||
}
|
||||
}
|
||||
|
||||
removeFromRecentlyOpened(toRemove: URI[]): void {
|
||||
const keep = (recent: IRecent) => {
|
||||
const uri = location(recent);
|
||||
for (const r of toRemove) {
|
||||
if (areResourcesEqual(r, uri)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const mru = this.getRecentlyOpened();
|
||||
const workspaces = mru.workspaces.filter(keep);
|
||||
const files = mru.files.filter(keep);
|
||||
|
||||
if (workspaces.length !== mru.workspaces.length || files.length !== mru.files.length) {
|
||||
this.saveRecentlyOpened({ files, workspaces });
|
||||
this._onRecentlyOpenedChange.fire();
|
||||
|
||||
// Schedule update to recent documents on macOS dock
|
||||
if (isMacintosh) {
|
||||
this.macOSRecentDocumentsUpdater.trigger(() => this.updateMacOSRecentDocuments());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateMacOSRecentDocuments(): Promise<void> {
|
||||
if (!isMacintosh) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We clear all documents first to ensure an up-to-date view on the set. Since entries
|
||||
// can get deleted on disk, this ensures that the list is always valid
|
||||
app.clearRecentDocuments();
|
||||
|
||||
const mru = this.getRecentlyOpened();
|
||||
|
||||
// Collect max-N recent workspaces that are known to exist
|
||||
const workspaceEntries: string[] = [];
|
||||
let entries = 0;
|
||||
for (let i = 0; i < mru.workspaces.length && entries < WorkspacesHistoryMainService.MAX_MACOS_DOCK_RECENT_WORKSPACES; i++) {
|
||||
const loc = location(mru.workspaces[i]);
|
||||
if (loc.scheme === Schemas.file) {
|
||||
const workspacePath = originalFSPath(loc);
|
||||
if (await exists(workspacePath)) {
|
||||
workspaceEntries.push(workspacePath);
|
||||
entries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect max-N recent files that are known to exist
|
||||
const fileEntries: string[] = [];
|
||||
for (let i = 0; i < mru.files.length && entries < WorkspacesHistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES_TOTAL; i++) {
|
||||
const loc = location(mru.files[i]);
|
||||
if (loc.scheme === Schemas.file) {
|
||||
const filePath = originalFSPath(loc);
|
||||
if (
|
||||
WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(loc)) !== -1 || // skip some well known file entries
|
||||
workspaceEntries.indexOf(filePath) !== -1 // prefer a workspace entry over a file entry (e.g. for .code-workspace)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await exists(filePath)) {
|
||||
fileEntries.push(filePath);
|
||||
entries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The apple guidelines (https://developer.apple.com/design/human-interface-guidelines/macos/menus/menu-anatomy/)
|
||||
// explain that most recent entries should appear close to the interaction by the user (e.g. close to the
|
||||
// mouse click). Most native macOS applications that add recent documents to the dock, show the most recent document
|
||||
// to the bottom (because the dock menu is not appearing from top to bottom, but from the bottom to the top). As such
|
||||
// we fill in the entries in reverse order so that the most recent shows up at the bottom of the menu.
|
||||
//
|
||||
// On top of that, the maximum number of documents can be configured by the user (defaults to 10). To ensure that
|
||||
// we are not failing to show the most recent entries, we start by adding files first (in reverse order of recency)
|
||||
// and then add folders (in reverse order of recency). Given that strategy, we can ensure that the most recent
|
||||
// N folders are always appearing, even if the limit is low (https://github.com/microsoft/vscode/issues/74788)
|
||||
fileEntries.reverse().forEach(fileEntry => app.addRecentDocument(fileEntry));
|
||||
workspaceEntries.reverse().forEach(workspaceEntry => app.addRecentDocument(workspaceEntry));
|
||||
}
|
||||
|
||||
clearRecentlyOpened(): void {
|
||||
this.saveRecentlyOpened({ workspaces: [], files: [] });
|
||||
app.clearRecentDocuments();
|
||||
|
||||
// Event
|
||||
this._onRecentlyOpenedChange.fire();
|
||||
}
|
||||
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
|
||||
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
|
||||
const files: IRecentFile[] = [];
|
||||
|
||||
// Add current workspace to beginning if set
|
||||
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
|
||||
workspaces.push({ workspace: currentWorkspace });
|
||||
}
|
||||
|
||||
if (currentFolder) {
|
||||
workspaces.push({ folderUri: currentFolder });
|
||||
}
|
||||
|
||||
// Add currently files to open to the beginning if any
|
||||
if (currentFiles) {
|
||||
for (let currentFile of currentFiles) {
|
||||
const fileUri = currentFile.fileUri;
|
||||
if (fileUri && indexOfFile(files, fileUri) === -1) {
|
||||
files.push({ fileUri });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addEntriesFromStorage(workspaces, files);
|
||||
|
||||
return { workspaces, files };
|
||||
}
|
||||
|
||||
private addEntriesFromStorage(workspaces: Array<IRecentFolder | IRecentWorkspace>, files: IRecentFile[]) {
|
||||
|
||||
// Get from storage
|
||||
let recents = this.getRecentlyOpenedFromStorage();
|
||||
for (let recent of recents.workspaces) {
|
||||
let index = isRecentFolder(recent) ? indexOfFolder(workspaces, recent.folderUri) : indexOfWorkspace(workspaces, recent.workspace);
|
||||
if (index >= 0) {
|
||||
workspaces[index].label = workspaces[index].label || recent.label;
|
||||
} else {
|
||||
workspaces.push(recent);
|
||||
}
|
||||
}
|
||||
|
||||
for (let recent of recents.files) {
|
||||
let index = indexOfFile(files, recent.fileUri);
|
||||
if (index >= 0) {
|
||||
files[index].label = files[index].label || recent.label;
|
||||
} else {
|
||||
files.push(recent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getRecentlyOpenedFromStorage(): IRecentlyOpened {
|
||||
const storedRecents = this.stateService.getItem<RecentlyOpenedStorageData>(WorkspacesHistoryMainService.recentlyOpenedStorageKey);
|
||||
|
||||
return restoreRecentlyOpened(storedRecents, this.logService);
|
||||
}
|
||||
|
||||
private saveRecentlyOpened(recent: IRecentlyOpened): void {
|
||||
const serialized = toStoreData(recent);
|
||||
|
||||
this.stateService.setItem(WorkspacesHistoryMainService.recentlyOpenedStorageKey, serialized);
|
||||
}
|
||||
|
||||
updateWindowsJumpList(): void {
|
||||
if (!isWindows) {
|
||||
return; // only on windows
|
||||
}
|
||||
|
||||
const jumpList: JumpListCategory[] = [];
|
||||
|
||||
// Tasks
|
||||
jumpList.push({
|
||||
type: 'tasks',
|
||||
items: [
|
||||
{
|
||||
type: 'task',
|
||||
title: nls.localize('newWindow', "New Window"),
|
||||
description: nls.localize('newWindowDesc', "Opens a new window"),
|
||||
program: process.execPath,
|
||||
args: '-n', // force new window
|
||||
iconPath: process.execPath,
|
||||
iconIndex: 0
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Recent Workspaces
|
||||
if (this.getRecentlyOpened().workspaces.length > 0) {
|
||||
|
||||
// The user might have meanwhile removed items from the jump list and we have to respect that
|
||||
// so we need to update our list of recent paths with the choice of the user to not add them again
|
||||
// Also: Windows will not show our custom category at all if there is any entry which was removed
|
||||
// by the user! See https://github.com/Microsoft/vscode/issues/15052
|
||||
let toRemove: URI[] = [];
|
||||
for (let item of app.getJumpListSettings().removedItems) {
|
||||
const args = item.args;
|
||||
if (args) {
|
||||
const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args);
|
||||
if (match) {
|
||||
toRemove.push(URI.parse(match[2]));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.removeFromRecentlyOpened(toRemove);
|
||||
|
||||
// Add entries
|
||||
jumpList.push({
|
||||
type: 'custom',
|
||||
name: nls.localize('recentFolders', "Recent Workspaces"),
|
||||
items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => {
|
||||
const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri;
|
||||
const title = recent.label || getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome);
|
||||
|
||||
let description;
|
||||
let args;
|
||||
if (isSingleFolderWorkspaceIdentifier(workspace)) {
|
||||
description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService));
|
||||
args = `--folder-uri "${workspace.toString()}"`;
|
||||
} else {
|
||||
description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService));
|
||||
args = `--file-uri "${workspace.configPath.toString()}"`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'task',
|
||||
title,
|
||||
description,
|
||||
program: process.execPath,
|
||||
args,
|
||||
iconPath: 'explorer.exe', // simulate folder icon
|
||||
iconIndex: 0
|
||||
};
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// Recent
|
||||
jumpList.push({
|
||||
type: 'recent' // this enables to show files in the "recent" category
|
||||
});
|
||||
|
||||
try {
|
||||
app.setJumpList(jumpList);
|
||||
} catch (error) {
|
||||
this.logService.warn('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function location(recent: IRecent): URI {
|
||||
if (isRecentFolder(recent)) {
|
||||
return recent.folderUri;
|
||||
}
|
||||
|
||||
if (isRecentFile(recent)) {
|
||||
return recent.fileUri;
|
||||
}
|
||||
|
||||
return recent.workspace.configPath;
|
||||
}
|
||||
|
||||
function indexOfWorkspace(arr: IRecent[], workspace: IWorkspaceIdentifier): number {
|
||||
return arrays.firstIndex(arr, w => isRecentWorkspace(w) && w.workspace.id === workspace.id);
|
||||
}
|
||||
|
||||
function indexOfFolder(arr: IRecent[], folderURI: ISingleFolderWorkspaceIdentifier): number {
|
||||
return arrays.firstIndex(arr, f => isRecentFolder(f) && areResourcesEqual(f.folderUri, folderURI));
|
||||
}
|
||||
|
||||
function indexOfFile(arr: IRecentFile[], fileURI: URI): number {
|
||||
return arrays.firstIndex(arr, f => areResourcesEqual(f.fileUri, fileURI));
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import * as os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRecentlyOpened, isRecentFolder, IRecentFolder, IRecentWorkspace } from 'vs/platform/workspaces/common/workspacesHistory';
|
||||
import { toStoreData, restoreRecentlyOpened } from 'vs/platform/workspaces/common/workspacesHistoryStorage';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
|
||||
function toWorkspace(uri: URI): IWorkspaceIdentifier {
|
||||
return {
|
||||
id: '1234',
|
||||
configPath: uri
|
||||
};
|
||||
}
|
||||
function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void {
|
||||
assert.equal(u1 && u1.toString(), u2 && u2.toString(), message);
|
||||
}
|
||||
|
||||
function assertEqualWorkspace(w1: IWorkspaceIdentifier | undefined, w2: IWorkspaceIdentifier | undefined, message?: string): void {
|
||||
if (!w1 || !w2) {
|
||||
assert.equal(w1, w2, message);
|
||||
return;
|
||||
}
|
||||
assert.equal(w1.id, w2.id, message);
|
||||
assertEqualURI(w1.configPath, w2.configPath, message);
|
||||
}
|
||||
|
||||
function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyOpened, message?: string) {
|
||||
assert.equal(actual.files.length, expected.files.length, message);
|
||||
for (let i = 0; i < actual.files.length; i++) {
|
||||
assertEqualURI(actual.files[i].fileUri, expected.files[i].fileUri, message);
|
||||
assert.equal(actual.files[i].label, expected.files[i].label);
|
||||
}
|
||||
assert.equal(actual.workspaces.length, expected.workspaces.length, message);
|
||||
for (let i = 0; i < actual.workspaces.length; i++) {
|
||||
let expectedRecent = expected.workspaces[i];
|
||||
let actualRecent = actual.workspaces[i];
|
||||
if (isRecentFolder(actualRecent)) {
|
||||
assertEqualURI(actualRecent.folderUri, (<IRecentFolder>expectedRecent).folderUri, message);
|
||||
} else {
|
||||
assertEqualWorkspace(actualRecent.workspace, (<IRecentWorkspace>expectedRecent).workspace, message);
|
||||
}
|
||||
assert.equal(actualRecent.label, expectedRecent.label);
|
||||
}
|
||||
}
|
||||
|
||||
function assertRestoring(state: IRecentlyOpened, message?: string) {
|
||||
const stored = toStoreData(state);
|
||||
const restored = restoreRecentlyOpened(stored, new NullLogService());
|
||||
assertEqualRecentlyOpened(state, restored, message);
|
||||
}
|
||||
|
||||
const testWSPath = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'test.code-workspace'));
|
||||
const testFileURI = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'testFile.txt'));
|
||||
const testFolderURI = URI.file(path.join(os.tmpdir(), 'windowStateTest', 'testFolder'));
|
||||
|
||||
const testRemoteFolderURI = URI.parse('foo://bar/c/e');
|
||||
const testRemoteFileURI = URI.parse('foo://bar/c/d.txt');
|
||||
const testRemoteWSURI = URI.parse('foo://bar/c/test.code-workspace');
|
||||
|
||||
suite('History Storage', () => {
|
||||
test('storing and restoring', () => {
|
||||
let ro: IRecentlyOpened;
|
||||
ro = {
|
||||
files: [],
|
||||
workspaces: []
|
||||
};
|
||||
assertRestoring(ro, 'empty');
|
||||
ro = {
|
||||
files: [{ fileUri: testFileURI }],
|
||||
workspaces: []
|
||||
};
|
||||
assertRestoring(ro, 'file');
|
||||
ro = {
|
||||
files: [],
|
||||
workspaces: [{ folderUri: testFolderURI }]
|
||||
};
|
||||
assertRestoring(ro, 'folder');
|
||||
ro = {
|
||||
files: [],
|
||||
workspaces: [{ workspace: toWorkspace(testWSPath) }, { folderUri: testFolderURI }]
|
||||
};
|
||||
assertRestoring(ro, 'workspaces and folders');
|
||||
|
||||
ro = {
|
||||
files: [{ fileUri: testRemoteFileURI }],
|
||||
workspaces: [{ workspace: toWorkspace(testRemoteWSURI) }, { folderUri: testRemoteFolderURI }]
|
||||
};
|
||||
assertRestoring(ro, 'remote workspaces and folders');
|
||||
ro = {
|
||||
files: [{ label: 'abc', fileUri: testFileURI }],
|
||||
workspaces: [{ label: 'def', workspace: toWorkspace(testWSPath) }, { folderUri: testRemoteFolderURI }]
|
||||
};
|
||||
assertRestoring(ro, 'labels');
|
||||
});
|
||||
|
||||
test('open 1_25', () => {
|
||||
const v1_25_win = `{
|
||||
"workspaces": [
|
||||
{
|
||||
"id": "2fa677dbdf5f771e775af84dea9feaea",
|
||||
"configPath": "C:\\\\workspaces\\\\testing\\\\test.code-workspace"
|
||||
},
|
||||
"C:\\\\workspaces\\\\testing\\\\test-ext",
|
||||
{
|
||||
"id": "d87a0241f8abc86b95c4e5481ebcbf56",
|
||||
"configPath": "C:\\\\workspaces\\\\test.code-workspace"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"C:\\\\workspaces\\\\test.code-workspace",
|
||||
"C:\\\\workspaces\\\\testing\\\\test-ext\\\\.gitignore"
|
||||
]
|
||||
}`;
|
||||
|
||||
let actual = restoreRecentlyOpened(JSON.parse(v1_25_win), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ fileUri: URI.file('C:\\workspaces\\test.code-workspace') }, { fileUri: URI.file('C:\\workspaces\\testing\\test-ext\\.gitignore') }],
|
||||
workspaces: [
|
||||
{ workspace: { id: '2fa677dbdf5f771e775af84dea9feaea', configPath: URI.file('C:\\workspaces\\testing\\test.code-workspace') } },
|
||||
{ folderUri: URI.file('C:\\workspaces\\testing\\test-ext') },
|
||||
{ workspace: { id: 'd87a0241f8abc86b95c4e5481ebcbf56', configPath: URI.file('C:\\workspaces\\test.code-workspace') } }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(actual, expected, 'v1_31_win');
|
||||
});
|
||||
|
||||
test('open 1_31', () => {
|
||||
const v1_31_win = `{
|
||||
"workspaces2": [
|
||||
"file:///c%3A/workspaces/testing/test-ext",
|
||||
"file:///c%3A/WINDOWS/system32",
|
||||
{
|
||||
"id": "d87a0241f8abc86b95c4e5481ebcbf56",
|
||||
"configPath": "c:\\\\workspaces\\\\test.code-workspace"
|
||||
}
|
||||
],
|
||||
"files2": [
|
||||
"file:///c%3A/workspaces/vscode/.yarnrc"
|
||||
]
|
||||
}`;
|
||||
|
||||
let actual = restoreRecentlyOpened(JSON.parse(v1_31_win), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ fileUri: URI.parse('file:///c%3A/workspaces/vscode/.yarnrc') }],
|
||||
workspaces: [
|
||||
{ folderUri: URI.parse('file:///c%3A/workspaces/testing/test-ext') },
|
||||
{ folderUri: URI.parse('file:///c%3A/WINDOWS/system32') },
|
||||
{ workspace: { id: 'd87a0241f8abc86b95c4e5481ebcbf56', configPath: URI.file('c:\\workspaces\\test.code-workspace') } }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(actual, expected, 'v1_31_win');
|
||||
});
|
||||
|
||||
test('open 1_32', () => {
|
||||
const v1_32 = `{
|
||||
"workspaces3": [
|
||||
{
|
||||
"id": "53b714b46ef1a2d4346568b4f591028c",
|
||||
"configURIPath": "file:///home/user/workspaces/testing/custom.code-workspace"
|
||||
},
|
||||
"file:///home/user/workspaces/testing/folding"
|
||||
],
|
||||
"files2": [
|
||||
"file:///home/user/.config/code-oss-dev/storage.json"
|
||||
]
|
||||
}`;
|
||||
|
||||
let windowsState = restoreRecentlyOpened(JSON.parse(v1_32), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ fileUri: URI.parse('file:///home/user/.config/code-oss-dev/storage.json') }],
|
||||
workspaces: [
|
||||
{ workspace: { id: '53b714b46ef1a2d4346568b4f591028c', configPath: URI.parse('file:///home/user/workspaces/testing/custom.code-workspace') } },
|
||||
{ folderUri: URI.parse('file:///home/user/workspaces/testing/folding') }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(windowsState, expected, 'v1_32');
|
||||
});
|
||||
|
||||
test('open 1_33', () => {
|
||||
const v1_33 = `{
|
||||
"workspaces3": [
|
||||
{
|
||||
"id": "53b714b46ef1a2d4346568b4f591028c",
|
||||
"configURIPath": "file:///home/user/workspaces/testing/custom.code-workspace"
|
||||
},
|
||||
"file:///home/user/workspaces/testing/folding"
|
||||
],
|
||||
"files2": [
|
||||
"file:///home/user/.config/code-oss-dev/storage.json"
|
||||
],
|
||||
"workspaceLabels": [
|
||||
null,
|
||||
"abc"
|
||||
],
|
||||
"fileLabels": [
|
||||
"def"
|
||||
]
|
||||
}`;
|
||||
|
||||
let windowsState = restoreRecentlyOpened(JSON.parse(v1_33), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ label: 'def', fileUri: URI.parse('file:///home/user/.config/code-oss-dev/storage.json') }],
|
||||
workspaces: [
|
||||
{ workspace: { id: '53b714b46ef1a2d4346568b4f591028c', configPath: URI.parse('file:///home/user/workspaces/testing/custom.code-workspace') } },
|
||||
{ label: 'abc', folderUri: URI.parse('file:///home/user/workspaces/testing/folding') }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(windowsState, expected, 'v1_33');
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user