Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,402 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace';
import { URI, UriComponents } from 'vs/base/common/uri';
import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform';
import { extname, isAbsolute } from 'vs/base/common/path';
import { dirname, resolvePath, isEqualAuthority, relativePath, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import * as jsonEdit from 'vs/base/common/jsonEdit';
import * as json from 'vs/base/common/json';
import { Schemas } from 'vs/base/common/network';
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 { ILogService } from 'vs/platform/log/common/log';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export const WORKSPACE_EXTENSION = 'code-workspace';
export const WORKSPACE_FILTER = [{ name: localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }];
export const UNTITLED_WORKSPACE_NAME = 'workspace.json';
export const IWorkspacesService = createDecorator<IWorkspacesService>('workspacesService');
export interface IWorkspacesService {
readonly _serviceBrand: undefined;
// 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>;
// 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 {
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');
}
/**
* A single folder workspace identifier is just the path to the folder.
*/
export type ISingleFolderWorkspaceIdentifier = URI;
export interface IWorkspaceIdentifier {
id: string;
configPath: URI;
}
export function reviveWorkspaceIdentifier(workspace: { id: string, configPath: UriComponents; }): IWorkspaceIdentifier {
return { id: workspace.id, configPath: URI.revive(workspace.configPath) };
}
export function isStoredWorkspaceFolder(thing: unknown): thing is IStoredWorkspaceFolder {
return isRawFileWorkspaceFolder(thing) || isRawUriWorkspaceFolder(thing);
}
export function isRawFileWorkspaceFolder(thing: any): thing is IRawFileWorkspaceFolder {
return thing
&& typeof thing === 'object'
&& typeof thing.path === 'string'
&& (!thing.name || typeof thing.name === 'string');
}
export function isRawUriWorkspaceFolder(thing: any): thing is IRawUriWorkspaceFolder {
return thing
&& typeof thing === 'object'
&& typeof thing.uri === 'string'
&& (!thing.name || typeof thing.name === 'string');
}
export interface IRawFileWorkspaceFolder {
path: string;
name?: string;
}
export interface IRawUriWorkspaceFolder {
uri: string;
name?: string;
}
export type IStoredWorkspaceFolder = IRawFileWorkspaceFolder | IRawUriWorkspaceFolder;
export interface IResolvedWorkspace extends IWorkspaceIdentifier {
folders: IWorkspaceFolder[];
remoteAuthority?: string;
}
export interface IStoredWorkspace {
folders: IStoredWorkspaceFolder[];
remoteAuthority?: string;
}
export interface IWorkspaceFolderCreationData {
uri: URI;
name?: string;
}
export interface IUntitledWorkspaceInfo {
workspace: IWorkspaceIdentifier;
remoteAuthority?: string;
}
export interface IEnterWorkspaceResult {
workspace: IWorkspaceIdentifier;
backupPath?: string;
}
export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier {
return obj instanceof URI;
}
export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier {
const workspaceIdentifier = obj as IWorkspaceIdentifier;
return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && workspaceIdentifier.configPath instanceof URI;
}
export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined {
if (workspace.configuration) {
return {
configPath: workspace.configuration,
id: workspace.id
};
}
if (workspace.folders.length === 1) {
return workspace.folders[0].uri;
}
// Empty workspace
return undefined;
}
export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean {
return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome);
}
export type IMultiFolderWorkspaceInitializationPayload = IWorkspaceIdentifier;
export interface ISingleFolderWorkspaceInitializationPayload { id: string; folder: ISingleFolderWorkspaceIdentifier; }
export interface IEmptyWorkspaceInitializationPayload { id: string; }
export type IWorkspaceInitializationPayload = IMultiFolderWorkspaceInitializationPayload | ISingleFolderWorkspaceInitializationPayload | IEmptyWorkspaceInitializationPayload;
export function isSingleFolderWorkspaceInitializationPayload(obj: any): obj is ISingleFolderWorkspaceInitializationPayload {
return isSingleFolderWorkspaceIdentifier((obj.folder as ISingleFolderWorkspaceIdentifier));
}
const WORKSPACE_SUFFIX = '.' + WORKSPACE_EXTENSION;
export function hasWorkspaceFileExtension(path: string | URI) {
const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path);
return ext === WORKSPACE_SUFFIX;
}
const SLASH = '/';
/**
* Given a folder URI and the workspace config folder, computes the IStoredWorkspaceFolder using
* a relative or absolute path or a uri.
* Undefined is returned if the folderURI and the targetConfigFolderURI don't have the same schema or authority
*
* @param folderURI a workspace folder
* @param forceAbsolute if set, keep the path absolute
* @param folderName a workspace name
* @param targetConfigFolderURI the folder where the workspace is living in
* @param useSlashForPath if set, use forward slashes for file paths on windows
*/
export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder {
if (folderURI.scheme !== targetConfigFolderURI.scheme) {
return { name: folderName, uri: folderURI.toString(true) };
}
let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined;
if (folderPath !== undefined) {
if (folderPath.length === 0) {
folderPath = '.';
} else if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) {
// Windows gets special treatment:
// - use backslahes unless slash is used by other existing folders
folderPath = folderPath.replace(/\//g, '\\');
}
} else {
// use absolute path
if (folderURI.scheme === Schemas.file) {
folderPath = folderURI.fsPath;
if (isWindows) {
// Windows gets special treatment:
// - normalize all paths to get nice casing of drive letters
// - use backslahes unless slash is used by other existing folders
folderPath = normalizeDriveLetter(folderPath);
if (useSlashForPath) {
folderPath = toSlashes(folderPath);
}
}
} else {
if (!isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) {
return { name: folderName, uri: folderURI.toString(true) };
}
folderPath = folderURI.path;
}
}
return { name: folderName, path: folderPath };
}
/**
* Rewrites the content of a workspace file to be saved at a new location.
* Throws an exception if file is not a valid workspace file
*/
export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) {
let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents);
const sourceConfigFolder = dirname(configPathURI);
const targetConfigFolder = dirname(targetConfigPathURI);
const rewrittenFolders: IStoredWorkspaceFolder[] = [];
const slashForPath = useSlashForPath(storedWorkspace.folders);
for (const folder of storedWorkspace.folders) {
const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri);
let absolute;
if (isFromUntitledWorkspace) {
// if it was an untitled workspace, try to make paths relative
absolute = false;
} else {
// for existing workspaces, preserve whether a path was absolute or relative
absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path);
}
rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath));
}
// Preserve as much of the existing workspace as possible by using jsonEdit
// and only changing the folders portion.
const formattingOptions: FormattingOptions = { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' };
const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], rewrittenFolders, formattingOptions);
let newContent = jsonEdit.applyEdits(rawWorkspaceContents, edits);
if (storedWorkspace.remoteAuthority === getRemoteAuthority(targetConfigPathURI)) {
// unsaved remote workspaces have the remoteAuthority set. Remove it when no longer nexessary.
newContent = jsonEdit.applyEdits(newContent, jsonEdit.removeProperty(newContent, ['remoteAuthority'], formattingOptions));
}
return newContent;
}
function doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace {
// Parse workspace file
let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser
// Filter out folders which do not have a path or uri set
if (storedWorkspace && Array.isArray(storedWorkspace.folders)) {
storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder));
} else {
throw new Error(`${path} looks like an invalid workspace file.`);
}
return storedWorkspace;
}
export function useSlashForPath(storedFolders: IStoredWorkspaceFolder[]): boolean {
if (isWindows) {
return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf(SLASH) >= 0);
}
return true;
}
//#region Workspace Storage
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 ISerializedWorkspace { id: string; configURIPath: 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;
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) });
}
});
}
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) });
}
});
}
}
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;
}
//#endregion

View File

@@ -0,0 +1,435 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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, splitName } from 'vs/base/common/labels';
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, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { ThrottledDelayer } from 'vs/base/common/async';
import { isEqual, dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
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');
export interface IWorkspacesHistoryMainService {
readonly _serviceBrand: undefined;
readonly onRecentlyOpenedChange: CommonEvent<void>;
addRecentlyOpened(recents: IRecent[]): void;
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened;
removeRecentlyOpened(paths: URI[]): void;
clearRecentlyOpened(): void;
updateWindowsJumpList(): void;
}
export class WorkspacesHistoryMainService extends Disposable 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';
declare readonly _serviceBrand: undefined;
private readonly _onRecentlyOpenedChange = new Emitter<void>();
readonly onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer<void>(800));
constructor(
@IStateService private readonly stateService: IStateService,
@ILogService private readonly logService: ILogService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
// Install window jump list after opening window
this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList());
// Add to history when entering workspace
this._register(this.workspacesMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }])));
}
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());
}
}
removeRecentlyOpened(toRemove: URI[]): void {
const keep = (recent: IRecent) => {
const uri = location(recent);
for (const resource of toRemove) {
if (isEqual(resource, 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.includes(basename(loc)) || // skip some well known file entries
workspaceEntries.includes(filePath) // 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(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;
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.removeRecentlyOpened(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 ? splitName(recent.label).name : this.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
}
}
private getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string {
if (isSingleFolderWorkspaceIdentifier(workspace)) {
return basename(workspace);
}
// Workspace: Untitled
if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, workspaceHome)) {
return nls.localize('untitledWorkspace', "Untitled (Workspace)");
}
let filename = basename(workspace.configPath);
if (filename.endsWith(WORKSPACE_EXTENSION)) {
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
}
return nls.localize('workspaceName', "{0} (Workspace)", filename);
}
}
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[], candidate: IWorkspaceIdentifier): number {
return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id);
}
function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number {
return arr.findIndex(folder => isRecentFolder(folder) && isEqual(folder.folderUri, candidate));
}
function indexOfFile(arr: IRecentFile[], candidate: URI): number {
return arr.findIndex(file => isEqual(file.fileUri, candidate));
}

View File

@@ -0,0 +1,339 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { join, dirname } from 'vs/base/common/path';
import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs';
import { readFileSync, existsSync, mkdirSync } from 'fs';
import { isLinux } from 'vs/base/common/platform';
import { Event, Emitter } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { createHash } from 'crypto';
import * as json from 'vs/base/common/json';
import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { originalFSPath, joinPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { localize } from 'vs/nls';
import product from 'vs/platform/product/common/product';
import { MessageBoxOptions, BrowserWindow } from 'electron';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { findWindowOnWorkspace } from 'vs/platform/windows/node/window';
export const IWorkspacesMainService = createDecorator<IWorkspacesMainService>('workspacesMainService');
export interface IWorkspaceEnteredEvent {
window: ICodeWindow;
workspace: IWorkspaceIdentifier;
}
export interface IWorkspacesMainService {
readonly _serviceBrand: undefined;
readonly onUntitledWorkspaceDeleted: Event<IWorkspaceIdentifier>;
readonly onWorkspaceEntered: Event<IWorkspaceEnteredEvent>;
enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise<IEnterWorkspaceResult | null>;
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier;
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void;
getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[];
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean;
resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null;
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;
}
export interface IStoredWorkspace {
folders: IStoredWorkspaceFolder[];
remoteAuthority?: string;
}
export class WorkspacesMainService extends Disposable implements IWorkspacesMainService {
declare readonly _serviceBrand: undefined;
private readonly untitledWorkspacesHome: URI; // local URI that contains all untitled workspaces
private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter<IWorkspaceIdentifier>());
readonly onUntitledWorkspaceDeleted: Event<IWorkspaceIdentifier> = this._onUntitledWorkspaceDeleted.event;
private readonly _onWorkspaceEntered = this._register(new Emitter<IWorkspaceEnteredEvent>());
readonly onWorkspaceEntered: Event<IWorkspaceEnteredEvent> = this._onWorkspaceEntered.event;
constructor(
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
@ILogService private readonly logService: ILogService,
@IBackupMainService private readonly backupMainService: IBackupMainService,
@IDialogMainService private readonly dialogMainService: IDialogMainService
) {
super();
this.untitledWorkspacesHome = environmentService.untitledWorkspacesHome;
}
resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null {
if (!this.isWorkspacePath(uri)) {
return null; // does not look like a valid workspace config file
}
if (uri.scheme !== Schemas.file) {
return null;
}
let contents: string;
try {
contents = readFileSync(uri.fsPath, 'utf8');
} catch (error) {
return null; // invalid workspace
}
return this.doResolveWorkspace(uri, contents);
}
private isWorkspacePath(uri: URI): boolean {
return isUntitledWorkspace(uri, this.environmentService) || hasWorkspaceFileExtension(uri);
}
private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null {
try {
const workspace = this.doParseStoredWorkspace(path, contents);
const workspaceIdentifier = getWorkspaceIdentifier(path);
return {
id: workspaceIdentifier.id,
configPath: workspaceIdentifier.configPath,
folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath),
remoteAuthority: workspace.remoteAuthority
};
} catch (error) {
this.logService.warn(error.toString());
}
return null;
}
private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace {
// Parse workspace file
let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser
// Filter out folders which do not have a path or uri set
if (storedWorkspace && Array.isArray(storedWorkspace.folders)) {
storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder));
} else {
throw new Error(`${path.toString(true)} looks like an invalid workspace file.`);
}
return storedWorkspace;
}
async createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier> {
const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority);
const configPath = workspace.configPath.fsPath;
await mkdirp(dirname(configPath));
await writeFile(configPath, JSON.stringify(storedWorkspace, null, '\t'));
return workspace;
}
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): IWorkspaceIdentifier {
const { workspace, storedWorkspace } = this.newUntitledWorkspace(folders, remoteAuthority);
const configPath = workspace.configPath.fsPath;
const configPathDir = dirname(configPath);
if (!existsSync(configPathDir)) {
const configPathDirDir = dirname(configPathDir);
if (!existsSync(configPathDirDir)) {
mkdirSync(configPathDirDir);
}
mkdirSync(configPathDir);
}
writeFileSync(configPath, JSON.stringify(storedWorkspace, null, '\t'));
return workspace;
}
private newUntitledWorkspace(folders: IWorkspaceFolderCreationData[] = [], remoteAuthority?: string): { workspace: IWorkspaceIdentifier, storedWorkspace: IStoredWorkspace } {
const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString();
const untitledWorkspaceConfigFolder = joinPath(this.untitledWorkspacesHome, randomId);
const untitledWorkspaceConfigPath = joinPath(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME);
const storedWorkspaceFolder: IStoredWorkspaceFolder[] = [];
for (const folder of folders) {
storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder));
}
return {
workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath),
storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority }
};
}
async getWorkspaceIdentifier(configPath: URI): Promise<IWorkspaceIdentifier> {
return getWorkspaceIdentifier(configPath);
}
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean {
return isUntitledWorkspace(workspace.configPath, this.environmentService);
}
deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
if (!this.isUntitledWorkspace(workspace)) {
return; // only supported for untitled workspaces
}
// Delete from disk
this.doDeleteUntitledWorkspaceSync(workspace);
// Event
this._onUntitledWorkspaceDeleted.fire(workspace);
}
async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> {
this.deleteUntitledWorkspaceSync(workspace);
}
private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
const configPath = originalFSPath(workspace.configPath);
try {
// Delete Workspace
rimrafSync(dirname(configPath));
// Mark Workspace Storage to be deleted
const workspaceStoragePath = join(this.environmentService.workspaceStorageHome.fsPath, workspace.id);
if (existsSync(workspaceStoragePath)) {
writeFileSync(join(workspaceStoragePath, 'obsolete'), '');
}
} catch (error) {
this.logService.warn(`Unable to delete untitled workspace ${configPath} (${error}).`);
}
}
getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] {
let untitledWorkspaces: IUntitledWorkspaceInfo[] = [];
try {
const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME));
for (const untitledWorkspacePath of untitledWorkspacePaths) {
const workspace = getWorkspaceIdentifier(untitledWorkspacePath);
const resolvedWorkspace = this.resolveLocalWorkspaceSync(untitledWorkspacePath);
if (!resolvedWorkspace) {
this.doDeleteUntitledWorkspaceSync(workspace);
} else {
untitledWorkspaces.push({ workspace, remoteAuthority: resolvedWorkspace.remoteAuthority });
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`);
}
}
return untitledWorkspaces;
}
async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise<IEnterWorkspaceResult | null> {
if (!window || !window.win || !window.isReady) {
return null; // return early if the window is not ready or disposed
}
const isValid = await this.isValidTargetWorkspacePath(window, windows, path);
if (!isValid) {
return null; // return early if the workspace is not valid
}
const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path));
if (!result) {
return null;
}
// Emit as event
this._onWorkspaceEntered.fire({ window, workspace: result.workspace });
return result;
}
private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise<boolean> {
if (!path) {
return true;
}
if (window.openedWorkspace && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, path)) {
return false; // window is already opened on a workspace with that path
}
// Prevent overwriting a workspace that is currently opened in another window
if (findWindowOnWorkspace(windows, getWorkspaceIdentifier(path))) {
const options: MessageBoxOptions = {
title: product.nameLong,
type: 'info',
buttons: [localize('ok', "OK")],
message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)),
detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."),
noLink: true
};
await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
return false;
}
return true; // OK
}
private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult | null {
if (!window.config) {
return null;
}
window.focus();
// Register window for backups and migrate current backups over
let backupPath: string | undefined;
if (!window.config.extensionDevelopmentPath) {
backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath);
}
// if the window was opened on an untitled workspace, delete it.
if (window.openedWorkspace && this.isUntitledWorkspace(window.openedWorkspace)) {
this.deleteUntitledWorkspaceSync(window.openedWorkspace);
}
// Update window configuration properly based on transition to workspace
window.config.folderUri = undefined;
window.config.workspace = workspace;
window.config.backupPath = backupPath;
return { workspace, backupPath };
}
}
function getWorkspaceId(configPath: URI): string {
let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString();
if (!isLinux) {
workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system
}
return createHash('md5').update(workspaceConfigPath).digest('hex');
}
export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier {
return {
configPath,
id: getWorkspaceId(configPath)
};
}

View File

@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
import { IWorkspacesService, IEnterWorkspaceResult, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IRecentlyOpened, IRecent } from 'vs/platform/workspaces/common/workspaces';
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 */> {
declare readonly _serviceBrand: undefined;
constructor(
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
@IBackupMainService private readonly backupMainService: IBackupMainService
) {
}
//#region Workspace Management
async enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | null> {
const window = this.windowsMainService.getWindowById(windowId);
if (window) {
return this.workspacesMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path);
}
return null;
}
createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier> {
return this.workspacesMainService.createUntitledWorkspace(folders, remoteAuthority);
}
deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise<void> {
return this.workspacesMainService.deleteUntitledWorkspace(workspace);
}
getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise<IWorkspaceIdentifier> {
return this.workspacesMainService.getWorkspaceIdentifier(workspacePath);
}
//#endregion
//#region Workspaces History
readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange;
async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> {
return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId));
}
async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise<void> {
return this.workspacesHistoryMainService.addRecentlyOpened(recents);
}
async removeRecentlyOpened(windowId: number, paths: URI[]): Promise<void> {
return this.workspacesHistoryMainService.removeRecentlyOpened(paths);
}
async clearRecentlyOpened(windowId: number): Promise<void> {
return this.workspacesHistoryMainService.clearRecentlyOpened();
}
//#endregion
//#region Dirty Workspaces
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
return this.backupMainService.getDirtyWorkspaces();
}
//#endregion
}

View File

@@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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, IRecentlyOpened, isRecentFolder, IRecentFolder, IRecentWorkspace, toStoreData, restoreRecentlyOpened } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
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_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');
});
});

View File

@@ -0,0 +1,457 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { EnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { NullLogService } from 'vs/platform/log/common/log';
import { URI } from 'vs/base/common/uri';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { isWindows } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { dirname, joinPath } from 'vs/base/common/resources';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
export class TestDialogMainService implements IDialogMainService {
declare readonly _serviceBrand: undefined;
pickFileFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
throw new Error('Method not implemented.');
}
pickFolder(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
throw new Error('Method not implemented.');
}
pickFile(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
throw new Error('Method not implemented.');
}
pickWorkspace(options: INativeOpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<string[] | undefined> {
throw new Error('Method not implemented.');
}
showMessageBox(options: Electron.MessageBoxOptions, window?: Electron.BrowserWindow | undefined): Promise<Electron.MessageBoxReturnValue> {
throw new Error('Method not implemented.');
}
showSaveDialog(options: Electron.SaveDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<Electron.SaveDialogReturnValue> {
throw new Error('Method not implemented.');
}
showOpenDialog(options: Electron.OpenDialogOptions, window?: Electron.BrowserWindow | undefined): Promise<Electron.OpenDialogReturnValue> {
throw new Error('Method not implemented.');
}
}
export class TestBackupMainService implements IBackupMainService {
declare readonly _serviceBrand: undefined;
isHotExitEnabled(): boolean {
throw new Error('Method not implemented.');
}
getWorkspaceBackups(): IWorkspaceBackupInfo[] {
throw new Error('Method not implemented.');
}
getFolderBackupPaths(): URI[] {
throw new Error('Method not implemented.');
}
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
throw new Error('Method not implemented.');
}
registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string | undefined): string {
throw new Error('Method not implemented.');
}
registerFolderBackupSync(folderUri: URI): string {
throw new Error('Method not implemented.');
}
registerEmptyWindowBackupSync(backupFolder?: string | undefined, remoteAuthority?: string | undefined): string {
throw new Error('Method not implemented.');
}
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
throw new Error('Method not implemented.');
}
unregisterFolderBackupSync(folderUri: URI): void {
throw new Error('Method not implemented.');
}
unregisterEmptyWindowBackupSync(backupFolder: string): void {
throw new Error('Method not implemented.');
}
async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> {
return [];
}
}
suite('WorkspacesMainService', () => {
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice');
const untitledWorkspacesHomePath = path.join(parentDir, 'Workspaces');
class TestEnvironmentService extends EnvironmentMainService {
get untitledWorkspacesHome(): URI {
return URI.file(untitledWorkspacesHomePath);
}
}
function createUntitledWorkspace(folders: string[], names?: string[]) {
return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
}
function createWorkspace(workspaceConfigPath: string, folders: (string | URI)[], names?: string[]): void {
const ws: IStoredWorkspace = {
folders: []
};
for (let i = 0; i < folders.length; i++) {
const f = folders[i];
const s: IStoredWorkspaceFolder = f instanceof URI ? { uri: f.toString() } : { path: f };
if (names) {
s.name = names[i];
}
ws.folders.push(s);
}
fs.writeFileSync(workspaceConfigPath, JSON.stringify(ws));
}
function createUntitledWorkspaceSync(folders: string[], names?: string[]) {
return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
}
const environmentService = new TestEnvironmentService(parseArgs(process.argv, OPTIONS));
const logService = new NullLogService();
let service: WorkspacesMainService;
setup(async () => {
service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService());
// Delete any existing backups completely and then re-create it.
await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE);
return pfs.mkdirp(untitledWorkspacesHomePath);
});
teardown(() => {
return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE);
});
function assertPathEquals(p1: string, p2: string): void {
if (isWindows) {
p1 = normalizeDriveLetter(p1);
p2 = normalizeDriveLetter(p2);
}
assert.equal(p1, p2);
}
function assertEqualURI(u1: URI, u2: URI): void {
assert.equal(u1.toString(), u2.toString());
}
test('createWorkspace (folders)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
assert.ok(workspace);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
assert.ok(service.isUntitledWorkspace(workspace));
const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace);
assert.equal(ws.folders.length, 2);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd());
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, os.tmpdir());
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name);
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name);
});
test('createWorkspace (folders with name)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
assert.ok(workspace);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
assert.ok(service.isUntitledWorkspace(workspace));
const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace);
assert.equal(ws.folders.length, 2);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd());
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, os.tmpdir());
assert.equal((<IRawFileWorkspaceFolder>ws.folders[0]).name, 'currentworkingdirectory');
assert.equal((<IRawFileWorkspaceFolder>ws.folders[1]).name, 'tempdir');
});
test('createUntitledWorkspace (folders as other resource URIs)', async () => {
const folder1URI = URI.parse('myscheme://server/work/p/f1');
const folder2URI = URI.parse('myscheme://server/work/o/f3');
const workspace = await service.createUntitledWorkspace([{ uri: folder1URI }, { uri: folder2URI }], 'server');
assert.ok(workspace);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
assert.ok(service.isUntitledWorkspace(workspace));
const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace);
assert.equal(ws.folders.length, 2);
assert.equal((<IRawUriWorkspaceFolder>ws.folders[0]).uri, folder1URI.toString(true));
assert.equal((<IRawUriWorkspaceFolder>ws.folders[1]).uri, folder2URI.toString(true));
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name);
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name);
assert.equal(ws.remoteAuthority, 'server');
});
test('createWorkspaceSync (folders)', () => {
const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()]);
assert.ok(workspace);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
assert.ok(service.isUntitledWorkspace(workspace));
const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace;
assert.equal(ws.folders.length, 2);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd());
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, os.tmpdir());
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name);
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name);
});
test('createWorkspaceSync (folders with names)', () => {
const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
assert.ok(workspace);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
assert.ok(service.isUntitledWorkspace(workspace));
const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace;
assert.equal(ws.folders.length, 2);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd());
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, os.tmpdir());
assert.equal((<IRawFileWorkspaceFolder>ws.folders[0]).name, 'currentworkingdirectory');
assert.equal((<IRawFileWorkspaceFolder>ws.folders[1]).name, 'tempdir');
});
test('createUntitledWorkspaceSync (folders as other resource URIs)', () => {
const folder1URI = URI.parse('myscheme://server/work/p/f1');
const folder2URI = URI.parse('myscheme://server/work/o/f3');
const workspace = service.createUntitledWorkspaceSync([{ uri: folder1URI }, { uri: folder2URI }]);
assert.ok(workspace);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
assert.ok(service.isUntitledWorkspace(workspace));
const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace;
assert.equal(ws.folders.length, 2);
assert.equal((<IRawUriWorkspaceFolder>ws.folders[0]).uri, folder1URI.toString(true));
assert.equal((<IRawUriWorkspaceFolder>ws.folders[1]).uri, folder2URI.toString(true));
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name);
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name);
});
test('resolveWorkspaceSync', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath));
// make it a valid workspace path
const newPath = path.join(path.dirname(workspace.configPath.fsPath), `workspace.${WORKSPACE_EXTENSION}`);
fs.renameSync(workspace.configPath.fsPath, newPath);
workspace.configPath = URI.file(newPath);
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assert.equal(2, resolved!.folders.length);
assertEqualURI(resolved!.configPath, workspace.configPath);
assert.ok(resolved!.id);
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace
const resolvedInvalid = service.resolveLocalWorkspaceSync(workspace.configPath);
assert.ok(!resolvedInvalid);
});
test('resolveWorkspaceSync (support relative paths)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] }));
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib')));
});
test('resolveWorkspaceSync (support relative paths #2)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] }));
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'other')));
});
test('resolveWorkspaceSync (support relative paths #3)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] }));
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib')));
});
test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
assertEqualURI(resolved!.folders[0].uri, URI.file(path.join(path.dirname(workspace.configPath.fsPath), 'ticino-playground', 'lib')));
});
test('rewriteWorkspaceFileForNewLocation', async () => {
const folder1 = process.cwd(); // absolute path because outside of tmpDir
const tmpDir = os.tmpdir();
const tmpInsideDir = path.join(tmpDir, 'inside');
const firstConfigPath = path.join(tmpDir, 'myworkspace0.code-workspace');
createWorkspace(firstConfigPath, [folder1, 'inside', path.join('inside', 'somefolder')]);
const origContent = fs.readFileSync(firstConfigPath).toString();
let origConfigPath = URI.file(firstConfigPath);
let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace'));
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath);
let ws = (JSON.parse(newContent) as IStoredWorkspace);
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1); // absolute path because outside of tmpdir
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, '.');
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, 'somefolder');
origConfigPath = workspaceConfigPath;
workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace'));
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
ws = (JSON.parse(newContent) as IStoredWorkspace);
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, 'inside');
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder');
origConfigPath = workspaceConfigPath;
workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace'));
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
ws = (JSON.parse(newContent) as IStoredWorkspace);
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, isWindows ? '..\\inside' : '../inside');
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, isWindows ? '..\\inside\\somefolder' : '../inside/somefolder');
origConfigPath = workspaceConfigPath;
workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace');
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
ws = (JSON.parse(newContent) as IStoredWorkspace);
assert.equal(ws.folders.length, 3);
assert.equal((<IRawUriWorkspaceFolder>ws.folders[0]).uri, URI.file(folder1).toString(true));
assert.equal((<IRawUriWorkspaceFolder>ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true));
assert.equal((<IRawUriWorkspaceFolder>ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true));
fs.unlinkSync(firstConfigPath);
});
test('rewriteWorkspaceFileForNewLocation (preserves comments)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
origContent = `// this is a comment\n${origContent}`;
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
assert.equal(0, newContent.indexOf('// this is a comment'));
service.deleteUntitledWorkspaceSync(workspace);
});
test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
const ws = (JSON.parse(newContent) as IStoredWorkspace);
assert.ok(ws.folders.every(f => (<IRawFileWorkspaceFolder>f).path.indexOf('\\') < 0));
service.deleteUntitledWorkspaceSync(workspace);
});
test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => {
if (!isWindows) {
return Promise.resolve();
}
const workspaceLocation = path.join(os.tmpdir(), 'wsloc');
const folder1Location = 'x:\\foo';
const folder2Location = '\\\\server\\share2\\some\\path';
const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more');
const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]);
const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
const ws = (JSON.parse(newContent) as IStoredWorkspace);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1Location);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, folder2Location);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, 'inner\\more');
service.deleteUntitledWorkspaceSync(workspace);
});
test('deleteUntitledWorkspaceSync (untitled)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
assert.ok(fs.existsSync(workspace.configPath.fsPath));
service.deleteUntitledWorkspaceSync(workspace);
assert.ok(!fs.existsSync(workspace.configPath.fsPath));
});
test('deleteUntitledWorkspaceSync (saved)', async () => {
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
service.deleteUntitledWorkspaceSync(workspace);
});
test('getUntitledWorkspaceSync', async function () {
this.retries(3);
let untitled = service.getUntitledWorkspacesSync();
assert.equal(untitled.length, 0);
const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
assert.ok(fs.existsSync(untitledOne.configPath.fsPath));
untitled = service.getUntitledWorkspacesSync();
assert.equal(1, untitled.length);
assert.equal(untitledOne.id, untitled[0].workspace.id);
const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]);
assert.ok(fs.existsSync(untitledTwo.configPath.fsPath));
assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`);
const untitledHome = dirname(dirname(untitledTwo.configPath));
const beforeGettingUntitledWorkspaces = fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8'));
untitled = service.getUntitledWorkspacesSync();
assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`);
if (untitled.length === 1) {
assert.fail(`Unexpected workspaces count of 1 (expected 2), all workspaces:\n ${fs.readdirSync(untitledHome.fsPath).map(name => fs.readFileSync(joinPath(untitledHome, name, 'workspace.json').fsPath, 'utf8'))}, before getUntitledWorkspacesSync: ${beforeGettingUntitledWorkspaces}`);
}
assert.equal(2, untitled.length);
service.deleteUntitledWorkspaceSync(untitledOne);
untitled = service.getUntitledWorkspacesSync();
assert.equal(1, untitled.length);
service.deleteUntitledWorkspaceSync(untitledTwo);
untitled = service.getUntitledWorkspacesSync();
assert.equal(0, untitled.length);
});
});