mirror of
https://github.com/coder/code-server.git
synced 2026-05-05 20:15:19 +02:00
chore(vscode): update to 1.53.2
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
This commit is contained in:
@@ -5,11 +5,11 @@
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { IWorkspaceFolder, IWorkspace, WorkspaceFolder } 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 { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri } 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';
|
||||
@@ -22,9 +22,16 @@ import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export const WORKSPACE_EXTENSION = 'code-workspace';
|
||||
const WORKSPACE_SUFFIX = `.${WORKSPACE_EXTENSION}`;
|
||||
export const WORKSPACE_FILTER = [{ name: localize('codeWorkspace', "Code Workspace"), extensions: [WORKSPACE_EXTENSION] }];
|
||||
export const UNTITLED_WORKSPACE_NAME = 'workspace.json';
|
||||
|
||||
export function hasWorkspaceFileExtension(path: string | URI) {
|
||||
const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path);
|
||||
|
||||
return ext === WORKSPACE_SUFFIX;
|
||||
}
|
||||
|
||||
export const IWorkspacesService = createDecorator<IWorkspacesService>('workspacesService');
|
||||
|
||||
export interface IWorkspacesService {
|
||||
@@ -48,6 +55,8 @@ export interface IWorkspacesService {
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
|
||||
}
|
||||
|
||||
//#region Workspaces Recently Opened
|
||||
|
||||
export interface IRecentlyOpened {
|
||||
workspaces: Array<IRecentWorkspace | IRecentFolder>;
|
||||
files: IRecentFile[];
|
||||
@@ -61,7 +70,7 @@ export interface IRecentWorkspace {
|
||||
}
|
||||
|
||||
export interface IRecentFolder {
|
||||
folderUri: ISingleFolderWorkspaceIdentifier;
|
||||
folderUri: URI;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@@ -82,36 +91,114 @@ 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;
|
||||
//#endregion
|
||||
|
||||
export interface IWorkspaceIdentifier {
|
||||
//#region Identifiers / Payload
|
||||
|
||||
export interface IBaseWorkspaceIdentifier {
|
||||
|
||||
/**
|
||||
* Every workspace (multi-root, single folder or empty)
|
||||
* has a unique identifier. It is not possible to open
|
||||
* a workspace with the same `id` in multiple windows
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single folder workspace identifier is a path to a folder + id.
|
||||
*/
|
||||
export interface ISingleFolderWorkspaceIdentifier extends IBaseWorkspaceIdentifier {
|
||||
|
||||
/**
|
||||
* Folder path as `URI`.
|
||||
*/
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier {
|
||||
const singleFolderIdentifier = obj as ISingleFolderWorkspaceIdentifier | undefined;
|
||||
|
||||
return typeof singleFolderIdentifier?.id === 'string' && URI.isUri(singleFolderIdentifier.uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* A multi-root workspace identifier is a path to a workspace file + id.
|
||||
*/
|
||||
export interface IWorkspaceIdentifier extends IBaseWorkspaceIdentifier {
|
||||
|
||||
/**
|
||||
* Workspace config file path as `URI`.
|
||||
*/
|
||||
configPath: URI;
|
||||
}
|
||||
|
||||
export function reviveWorkspaceIdentifier(workspace: { id: string, configPath: UriComponents; }): IWorkspaceIdentifier {
|
||||
return { id: workspace.id, configPath: URI.revive(workspace.configPath) };
|
||||
export function toWorkspaceIdentifier(workspace: IWorkspace): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined {
|
||||
|
||||
// Multi root
|
||||
if (workspace.configuration) {
|
||||
return {
|
||||
id: workspace.id,
|
||||
configPath: workspace.configuration
|
||||
};
|
||||
}
|
||||
|
||||
// Single folder
|
||||
if (workspace.folders.length === 1) {
|
||||
return {
|
||||
id: workspace.id,
|
||||
uri: workspace.folders[0].uri
|
||||
};
|
||||
}
|
||||
|
||||
// Empty workspace
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isStoredWorkspaceFolder(thing: unknown): thing is IStoredWorkspaceFolder {
|
||||
return isRawFileWorkspaceFolder(thing) || isRawUriWorkspaceFolder(thing);
|
||||
export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier {
|
||||
const workspaceIdentifier = obj as IWorkspaceIdentifier | undefined;
|
||||
|
||||
return typeof workspaceIdentifier?.id === 'string' && URI.isUri(workspaceIdentifier.configPath);
|
||||
}
|
||||
|
||||
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 reviveIdentifier(identifier: { id: string, uri?: UriComponents, configPath?: UriComponents } | undefined): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined {
|
||||
if (identifier?.uri) {
|
||||
return { id: identifier.id, uri: URI.revive(identifier.uri) };
|
||||
}
|
||||
|
||||
if (identifier?.configPath) {
|
||||
return { id: identifier.id, configPath: URI.revive(identifier.configPath) };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isRawUriWorkspaceFolder(thing: any): thing is IRawUriWorkspaceFolder {
|
||||
return thing
|
||||
&& typeof thing === 'object'
|
||||
&& typeof thing.uri === 'string'
|
||||
&& (!thing.name || typeof thing.name === 'string');
|
||||
export function isUntitledWorkspace(path: URI, environmentService: IEnvironmentService): boolean {
|
||||
return extUriBiasedIgnorePathCase.isEqualOrParent(path, environmentService.untitledWorkspacesHome);
|
||||
}
|
||||
|
||||
export interface IEmptyWorkspaceInitializationPayload extends IBaseWorkspaceIdentifier { }
|
||||
|
||||
export type IWorkspaceInitializationPayload = IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceInitializationPayload;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Workspace File Utilities
|
||||
|
||||
export function isStoredWorkspaceFolder(obj: unknown): obj is IStoredWorkspaceFolder {
|
||||
return isRawFileWorkspaceFolder(obj) || isRawUriWorkspaceFolder(obj);
|
||||
}
|
||||
|
||||
export function isRawFileWorkspaceFolder(obj: unknown): obj is IRawFileWorkspaceFolder {
|
||||
const candidate = obj as IRawFileWorkspaceFolder | undefined;
|
||||
|
||||
return typeof candidate?.path === 'string' && (!candidate.name || typeof candidate.name === 'string');
|
||||
}
|
||||
|
||||
export function isRawUriWorkspaceFolder(obj: unknown): obj is IRawUriWorkspaceFolder {
|
||||
const candidate = obj as IRawUriWorkspaceFolder | undefined;
|
||||
|
||||
return typeof candidate?.uri === 'string' && (!candidate.name || typeof candidate.name === 'string');
|
||||
}
|
||||
|
||||
export interface IRawFileWorkspaceFolder {
|
||||
@@ -151,56 +238,6 @@ export interface IEnterWorkspaceResult {
|
||||
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.
|
||||
@@ -212,12 +249,12 @@ const SLASH = '/';
|
||||
* @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 {
|
||||
export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows, extUri: IExtUri): IStoredWorkspaceFolder {
|
||||
if (folderURI.scheme !== targetConfigFolderURI.scheme) {
|
||||
return { name: folderName, uri: folderURI.toString(true) };
|
||||
}
|
||||
|
||||
let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined;
|
||||
let folderPath = !forceAbsolute ? extUri.relativePath(targetConfigFolderURI, folderURI) : undefined;
|
||||
if (folderPath !== undefined) {
|
||||
if (folderPath.length === 0) {
|
||||
folderPath = '.';
|
||||
@@ -241,7 +278,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) {
|
||||
if (!extUri.isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) {
|
||||
return { name: folderName, uri: folderURI.toString(true) };
|
||||
}
|
||||
folderPath = folderURI.path;
|
||||
@@ -251,21 +288,59 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean,
|
||||
return { name: folderName, path: folderPath };
|
||||
}
|
||||
|
||||
export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI, extUri: IExtUri): WorkspaceFolder[] {
|
||||
let result: WorkspaceFolder[] = [];
|
||||
let seen: Set<string> = new Set();
|
||||
|
||||
const relativeTo = extUri.dirname(workspaceConfigFile);
|
||||
for (let configuredFolder of configuredFolders) {
|
||||
let uri: URI | null = null;
|
||||
if (isRawFileWorkspaceFolder(configuredFolder)) {
|
||||
if (configuredFolder.path) {
|
||||
uri = extUri.resolvePath(relativeTo, configuredFolder.path);
|
||||
}
|
||||
} else if (isRawUriWorkspaceFolder(configuredFolder)) {
|
||||
try {
|
||||
uri = URI.parse(configuredFolder.uri);
|
||||
// this makes sure all workspace folder are absolute
|
||||
if (uri.path[0] !== '/') {
|
||||
uri = uri.with({ path: '/' + uri.path });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (uri) {
|
||||
// remove duplicates
|
||||
let comparisonKey = extUri.getComparisonKey(uri);
|
||||
if (!seen.has(comparisonKey)) {
|
||||
seen.add(comparisonKey);
|
||||
|
||||
const name = configuredFolder.name || extUri.basenameOrAuthority(uri);
|
||||
result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI, extUri: IExtUri) {
|
||||
let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents);
|
||||
|
||||
const sourceConfigFolder = dirname(configPathURI);
|
||||
const targetConfigFolder = dirname(targetConfigPathURI);
|
||||
const sourceConfigFolder = extUri.dirname(configPathURI);
|
||||
const targetConfigFolder = extUri.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);
|
||||
const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri);
|
||||
let absolute;
|
||||
if (isFromUntitledWorkspace) {
|
||||
// if it was an untitled workspace, try to make paths relative
|
||||
@@ -274,7 +349,7 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string,
|
||||
// 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));
|
||||
rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri));
|
||||
}
|
||||
|
||||
// Preserve as much of the existing workspace as possible by using jsonEdit
|
||||
@@ -308,12 +383,14 @@ function doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace {
|
||||
|
||||
export function useSlashForPath(storedFolders: IStoredWorkspaceFolder[]): boolean {
|
||||
if (isWindows) {
|
||||
return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf(SLASH) >= 0);
|
||||
return storedFolders.some(folder => isRawFileWorkspaceFolder(folder) && folder.path.indexOf('/') >= 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Workspace Storage
|
||||
|
||||
interface ISerializedRecentlyOpened {
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
* 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 { localize } from 'vs/nls';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { app, JumpListCategory } from 'electron';
|
||||
import { app, JumpListCategory, JumpListItem } 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 { IWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { isEqual, dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources';
|
||||
import { 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';
|
||||
@@ -57,15 +57,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onRecentlyOpenedChange = new Emitter<void>();
|
||||
private readonly _onRecentlyOpenedChange = this._register(new Emitter<void>());
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
|
||||
|
||||
private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer<void>(800));
|
||||
private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer<void>(800));
|
||||
|
||||
constructor(
|
||||
@IStateService private readonly stateService: IStateService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
|
||||
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService,
|
||||
@IEnvironmentMainService private readonly environmentService: IEnvironmentMainService,
|
||||
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService
|
||||
) {
|
||||
@@ -80,7 +80,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
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 }])));
|
||||
this._register(this.workspacesManagementMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }])));
|
||||
}
|
||||
|
||||
private handleWindowsJumpList(): void {
|
||||
@@ -89,40 +89,40 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
}
|
||||
|
||||
this.updateWindowsJumpList();
|
||||
this.onRecentlyOpenedChange(() => this.updateWindowsJumpList());
|
||||
this._register(this.onRecentlyOpenedChange(() => this.updateWindowsJumpList()));
|
||||
}
|
||||
|
||||
addRecentlyOpened(newlyAdded: IRecent[]): void {
|
||||
addRecentlyOpened(recentToAdd: IRecent[]): void {
|
||||
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
|
||||
const files: IRecentFile[] = [];
|
||||
|
||||
for (let curr of newlyAdded) {
|
||||
for (let recent of recentToAdd) {
|
||||
|
||||
// Workspace
|
||||
if (isRecentWorkspace(curr)) {
|
||||
if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) {
|
||||
workspaces.push(curr);
|
||||
if (isRecentWorkspace(recent)) {
|
||||
if (!this.workspacesManagementMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) {
|
||||
workspaces.push(recent);
|
||||
}
|
||||
}
|
||||
|
||||
// Folder
|
||||
else if (isRecentFolder(curr)) {
|
||||
if (indexOfFolder(workspaces, curr.folderUri) === -1) {
|
||||
workspaces.push(curr);
|
||||
else if (isRecentFolder(recent)) {
|
||||
if (indexOfFolder(workspaces, recent.folderUri) === -1) {
|
||||
workspaces.push(recent);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0;
|
||||
const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0;
|
||||
|
||||
if (!alreadyExistsInHistory && !shouldBeFiltered) {
|
||||
files.push(curr);
|
||||
files.push(recent);
|
||||
|
||||
// Add to recent documents (Windows only, macOS later)
|
||||
if (isWindows && curr.fileUri.scheme === Schemas.file) {
|
||||
app.addRecentDocument(curr.fileUri.fsPath);
|
||||
if (isWindows && recent.fileUri.scheme === Schemas.file) {
|
||||
app.addRecentDocument(recent.fileUri.fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,14 +147,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
}
|
||||
}
|
||||
|
||||
removeRecentlyOpened(toRemove: URI[]): void {
|
||||
removeRecentlyOpened(recentToRemove: URI[]): void {
|
||||
const keep = (recent: IRecent) => {
|
||||
const uri = location(recent);
|
||||
for (const resource of toRemove) {
|
||||
if (isEqual(resource, uri)) {
|
||||
for (const resourceToRemove of recentToRemove) {
|
||||
if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -246,13 +247,10 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
|
||||
// Add current workspace to beginning if set
|
||||
const currentWorkspace = include?.config?.workspace;
|
||||
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
|
||||
if (isWorkspaceIdentifier(currentWorkspace) && !this.workspacesManagementMainService.isUntitledWorkspace(currentWorkspace)) {
|
||||
workspaces.push({ workspace: currentWorkspace });
|
||||
}
|
||||
|
||||
const currentFolder = include?.config?.folderUri;
|
||||
if (currentFolder) {
|
||||
workspaces.push({ folderUri: currentFolder });
|
||||
} else if (isSingleFolderWorkspaceIdentifier(currentWorkspace)) {
|
||||
workspaces.push({ folderUri: currentWorkspace.uri });
|
||||
}
|
||||
|
||||
// Add currently files to open to the beginning if any
|
||||
@@ -319,8 +317,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
items: [
|
||||
{
|
||||
type: 'task',
|
||||
title: nls.localize('newWindow', "New Window"),
|
||||
description: nls.localize('newWindowDesc', "Opens a new window"),
|
||||
title: localize('newWindow', "New Window"),
|
||||
description: localize('newWindowDesc', "Opens a new window"),
|
||||
program: process.execPath,
|
||||
args: '-n', // force new window
|
||||
iconPath: process.execPath,
|
||||
@@ -330,57 +328,59 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
});
|
||||
|
||||
// Recent Workspaces
|
||||
try {
|
||||
if (this.getRecentlyOpened().workspaces.length > 0) {
|
||||
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]));
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
this.removeRecentlyOpened(toRemove);
|
||||
|
||||
// Add entries
|
||||
// Add entries
|
||||
let hasWorkspaces = false;
|
||||
const items: JumpListItem[] = 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 (URI.isUri(workspace)) {
|
||||
description = localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService));
|
||||
args = `--folder-uri "${workspace.toString()}"`;
|
||||
} else {
|
||||
hasWorkspaces = true;
|
||||
description = localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService));
|
||||
args = `--file-uri "${workspace.configPath.toString()}"`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'task',
|
||||
title: title.substr(0, 255), // Windows seems to be picky around the length of entries
|
||||
description: description.substr(0, 255), // (see https://github.com/microsoft/vscode/issues/111177)
|
||||
program: process.execPath,
|
||||
args,
|
||||
iconPath: 'explorer.exe', // simulate folder icon
|
||||
iconIndex: 0
|
||||
};
|
||||
}));
|
||||
|
||||
if (items.length > 0) {
|
||||
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
|
||||
};
|
||||
}))
|
||||
name: hasWorkspaces ? localize('recentFoldersAndWorkspaces', "Recent Folders & Workspaces") : localize('recentFolders', "Recent Folders"),
|
||||
items
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.warn('updateWindowsJumpList#recentWorkspaces', error); // https://github.com/microsoft/vscode/issues/111177
|
||||
}
|
||||
|
||||
// Recent
|
||||
@@ -396,21 +396,24 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
}
|
||||
|
||||
private getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string {
|
||||
if (isSingleFolderWorkspaceIdentifier(workspace)) {
|
||||
|
||||
// Single Folder
|
||||
if (URI.isUri(workspace)) {
|
||||
return basename(workspace);
|
||||
}
|
||||
|
||||
// Workspace: Untitled
|
||||
if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, workspaceHome)) {
|
||||
return nls.localize('untitledWorkspace', "Untitled (Workspace)");
|
||||
return localize('untitledWorkspace', "Untitled (Workspace)");
|
||||
}
|
||||
|
||||
// Workspace: normal
|
||||
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);
|
||||
return localize('workspaceName', "{0} (Workspace)", filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,10 +433,10 @@ function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): numb
|
||||
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 indexOfFolder(arr: IRecent[], candidate: URI): number {
|
||||
return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate));
|
||||
}
|
||||
|
||||
function indexOfFile(arr: IRecentFile[], candidate: URI): number {
|
||||
return arr.findIndex(file => isEqual(file.fileUri, candidate));
|
||||
return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate));
|
||||
}
|
||||
|
||||
@@ -3,337 +3,79 @@
|
||||
* 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 { 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 { 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 { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
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';
|
||||
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 {
|
||||
export class WorkspacesMainService implements AddFirstParameterToFunctions<IWorkspacesService, Promise<unknown> /* only methods, not events */, number /* window ID */> {
|
||||
|
||||
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
|
||||
@IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
|
||||
@IBackupMainService private readonly backupMainService: IBackupMainService
|
||||
) {
|
||||
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;
|
||||
}
|
||||
//#region Workspace Management
|
||||
|
||||
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());
|
||||
async enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | null> {
|
||||
const window = this.windowsMainService.getWindowById(windowId);
|
||||
if (window) {
|
||||
return this.workspacesManagementMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path);
|
||||
}
|
||||
|
||||
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;
|
||||
createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier> {
|
||||
return this.workspacesManagementMainService.createUntitledWorkspace(folders, remoteAuthority);
|
||||
}
|
||||
|
||||
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;
|
||||
deleteUntitledWorkspace(windowId: number, workspace: IWorkspaceIdentifier): Promise<void> {
|
||||
return this.workspacesManagementMainService.deleteUntitledWorkspace(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;
|
||||
getWorkspaceIdentifier(windowId: number, workspacePath: URI): Promise<IWorkspaceIdentifier> {
|
||||
return this.workspacesManagementMainService.getWorkspaceIdentifier(workspacePath);
|
||||
}
|
||||
|
||||
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);
|
||||
//#endregion
|
||||
|
||||
const storedWorkspaceFolder: IStoredWorkspaceFolder[] = [];
|
||||
//#region Workspaces History
|
||||
|
||||
for (const folder of folders) {
|
||||
storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder));
|
||||
}
|
||||
readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange;
|
||||
|
||||
return {
|
||||
workspace: getWorkspaceIdentifier(untitledWorkspaceConfigPath),
|
||||
storedWorkspace: { folders: storedWorkspaceFolder, remoteAuthority }
|
||||
};
|
||||
async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> {
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId));
|
||||
}
|
||||
|
||||
async getWorkspaceIdentifier(configPath: URI): Promise<IWorkspaceIdentifier> {
|
||||
return getWorkspaceIdentifier(configPath);
|
||||
async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise<void> {
|
||||
return this.workspacesHistoryMainService.addRecentlyOpened(recents);
|
||||
}
|
||||
|
||||
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean {
|
||||
return isUntitledWorkspace(workspace.configPath, this.environmentService);
|
||||
async removeRecentlyOpened(windowId: number, paths: URI[]): Promise<void> {
|
||||
return this.workspacesHistoryMainService.removeRecentlyOpened(paths);
|
||||
}
|
||||
|
||||
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 clearRecentlyOpened(windowId: number): Promise<void> {
|
||||
return this.workspacesHistoryMainService.clearRecentlyOpened();
|
||||
}
|
||||
|
||||
async deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> {
|
||||
this.deleteUntitledWorkspaceSync(workspace);
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Dirty Workspaces
|
||||
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
|
||||
return this.backupMainService.getDirtyWorkspaces();
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { toWorkspaceFolders, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult, isUntitledWorkspace, isWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } 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, statSync, Stats } from 'fs';
|
||||
import { isLinux, isMacintosh, isWindows } 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 { parse } from 'vs/base/common/json';
|
||||
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/dialogMainService';
|
||||
import { findWindowOnWorkspaceOrFolder } from 'vs/platform/windows/electron-main/windowsFinder';
|
||||
|
||||
export const IWorkspacesManagementMainService = createDecorator<IWorkspacesManagementMainService>('workspacesManagementMainService');
|
||||
|
||||
export interface IWorkspaceEnteredEvent {
|
||||
window: ICodeWindow;
|
||||
workspace: IWorkspaceIdentifier;
|
||||
}
|
||||
|
||||
export interface IWorkspacesManagementMainService {
|
||||
|
||||
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 WorkspacesManagementMainService extends Disposable implements IWorkspacesManagementMainService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly untitledWorkspacesHome = this.environmentService.untitledWorkspacesHome; // 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();
|
||||
}
|
||||
|
||||
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, extUriBiasedIgnorePathCase),
|
||||
remoteAuthority: workspace.remoteAuthority
|
||||
};
|
||||
} catch (error) {
|
||||
this.logService.warn(error.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace {
|
||||
|
||||
// Parse workspace file
|
||||
const storedWorkspace: IStoredWorkspace = 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, !isWindows, extUriBiasedIgnorePathCase));
|
||||
}
|
||||
|
||||
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[] {
|
||||
const 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[], workspacePath?: URI): Promise<boolean> {
|
||||
if (!workspacePath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isWorkspaceIdentifier(window.openedWorkspace) && extUriBiasedIgnorePathCase.isEqual(window.openedWorkspace.configPath, workspacePath)) {
|
||||
return false; // window is already opened on a workspace with that path
|
||||
}
|
||||
|
||||
// Prevent overwriting a workspace that is currently opened in another window
|
||||
if (findWindowOnWorkspaceOrFolder(windows, workspacePath)) {
|
||||
const options: MessageBoxOptions = {
|
||||
title: product.nameLong,
|
||||
type: 'info',
|
||||
buttons: [localize('ok', "OK")],
|
||||
message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(workspacePath)),
|
||||
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 (isWorkspaceIdentifier(window.openedWorkspace) && this.isUntitledWorkspace(window.openedWorkspace)) {
|
||||
this.deleteUntitledWorkspaceSync(window.openedWorkspace);
|
||||
}
|
||||
|
||||
// Update window configuration properly based on transition to workspace
|
||||
window.config.workspace = workspace;
|
||||
window.config.backupPath = backupPath;
|
||||
|
||||
return { workspace, backupPath };
|
||||
}
|
||||
}
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
export function getWorkspaceIdentifier(configPath: URI): IWorkspaceIdentifier {
|
||||
|
||||
function getWorkspaceId(): string {
|
||||
let configPathStr = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString();
|
||||
if (!isLinux) {
|
||||
configPathStr = configPathStr.toLowerCase(); // sanitize for platform file system
|
||||
}
|
||||
|
||||
return createHash('md5').update(configPathStr).digest('hex');
|
||||
}
|
||||
|
||||
return {
|
||||
id: getWorkspaceId(),
|
||||
configPath
|
||||
};
|
||||
}
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
export function getSingleFolderWorkspaceIdentifier(folderUri: URI): ISingleFolderWorkspaceIdentifier | undefined;
|
||||
export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat: Stats): ISingleFolderWorkspaceIdentifier;
|
||||
export function getSingleFolderWorkspaceIdentifier(folderUri: URI, folderStat?: Stats): ISingleFolderWorkspaceIdentifier | undefined {
|
||||
|
||||
function getFolderId(): string | undefined {
|
||||
|
||||
// Remote: produce a hash from the entire URI
|
||||
if (folderUri.scheme !== Schemas.file) {
|
||||
return createHash('md5').update(folderUri.toString()).digest('hex');
|
||||
}
|
||||
|
||||
// Local: produce a hash from the path and include creation time as salt
|
||||
if (!folderStat) {
|
||||
try {
|
||||
folderStat = statSync(folderUri.fsPath);
|
||||
} catch (error) {
|
||||
return undefined; // folder does not exist
|
||||
}
|
||||
}
|
||||
|
||||
let ctime: number | undefined;
|
||||
if (isLinux) {
|
||||
ctime = folderStat.ino; // Linux: birthtime is ctime, so we cannot use it! We use the ino instead!
|
||||
} else if (isMacintosh) {
|
||||
ctime = folderStat.birthtime.getTime(); // macOS: birthtime is fine to use as is
|
||||
} else if (isWindows) {
|
||||
if (typeof folderStat.birthtimeMs === 'number') {
|
||||
ctime = Math.floor(folderStat.birthtimeMs); // Windows: fix precision issue in node.js 8.x to get 7.x results (see https://github.com/nodejs/node/issues/19897)
|
||||
} else {
|
||||
ctime = folderStat.birthtime.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// we use the ctime as extra salt to the ID so that we catch the case of a folder getting
|
||||
// deleted and recreated. in that case we do not want to carry over previous state
|
||||
return createHash('md5').update(folderUri.fsPath).update(ctime ? String(ctime) : '').digest('hex');
|
||||
}
|
||||
|
||||
const folderId = getFolderId();
|
||||
if (typeof folderId === 'string') {
|
||||
return {
|
||||
id: folderId,
|
||||
uri: folderUri
|
||||
};
|
||||
}
|
||||
|
||||
return undefined; // invalid folder
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { URI } from 'vs/base/common/uri';
|
||||
import { hasWorkspaceFileExtension, toWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
|
||||
suite('Workspaces', () => {
|
||||
test('hasWorkspaceFileExtension', () => {
|
||||
assert.strictEqual(hasWorkspaceFileExtension('something'), false);
|
||||
assert.strictEqual(hasWorkspaceFileExtension('something.code-workspace'), true);
|
||||
});
|
||||
|
||||
test('toWorkspaceIdentifier', () => {
|
||||
let identifier = toWorkspaceIdentifier({ id: 'id', folders: [] });
|
||||
assert.ok(!identifier);
|
||||
assert.ok(!isSingleFolderWorkspaceIdentifier(identifier));
|
||||
assert.ok(!isWorkspaceIdentifier(identifier));
|
||||
|
||||
identifier = toWorkspaceIdentifier({ id: 'id', folders: [{ index: 0, name: 'test', toResource: () => URI.file('test'), uri: URI.file('test') }] });
|
||||
assert.ok(identifier);
|
||||
assert.ok(isSingleFolderWorkspaceIdentifier(identifier));
|
||||
assert.ok(!isWorkspaceIdentifier(identifier));
|
||||
|
||||
identifier = toWorkspaceIdentifier({ id: 'id', configuration: URI.file('test.code-workspace'), folders: [] });
|
||||
assert.ok(identifier);
|
||||
assert.ok(!isSingleFolderWorkspaceIdentifier(identifier));
|
||||
assert.ok(isWorkspaceIdentifier(identifier));
|
||||
});
|
||||
});
|
||||
@@ -17,25 +17,25 @@ function toWorkspace(uri: URI): IWorkspaceIdentifier {
|
||||
};
|
||||
}
|
||||
function assertEqualURI(u1: URI | undefined, u2: URI | undefined, message?: string): void {
|
||||
assert.equal(u1 && u1.toString(), u2 && u2.toString(), message);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(w1, w2, message);
|
||||
return;
|
||||
}
|
||||
assert.equal(w1.id, w2.id, message);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(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.strictEqual(actual.files[i].label, expected.files[i].label);
|
||||
}
|
||||
assert.equal(actual.workspaces.length, expected.workspaces.length, message);
|
||||
assert.strictEqual(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];
|
||||
@@ -44,7 +44,7 @@ function assertEqualRecentlyOpened(actual: IRecentlyOpened, expected: IRecentlyO
|
||||
} else {
|
||||
assertEqualWorkspace(actualRecent.workspace, (<IRecentWorkspace>expectedRecent).workspace, message);
|
||||
}
|
||||
assert.equal(actualRecent.label, expectedRecent.label);
|
||||
assert.strictEqual(actualRecent.label, expectedRecent.label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +129,5 @@ suite('History Storage', () => {
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(windowsState, expected, 'v1_33');
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -10,15 +10,15 @@ 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 { WorkspacesManagementMainService, IStoredWorkspace, getSingleFolderWorkspaceIdentifier, getWorkspaceIdentifier } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService';
|
||||
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 { dirname, extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources';
|
||||
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService';
|
||||
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';
|
||||
@@ -104,15 +104,7 @@ export class TestBackupMainService implements IBackupMainService {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
suite('WorkspacesManagementMainService', () => {
|
||||
|
||||
function createUntitledWorkspace(folders: string[], names?: string[]) {
|
||||
return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
|
||||
@@ -138,22 +130,31 @@ suite('WorkspacesMainService', () => {
|
||||
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;
|
||||
let testDir: string;
|
||||
let untitledWorkspacesHomePath: string;
|
||||
let environmentService: EnvironmentMainService;
|
||||
let service: WorkspacesManagementMainService;
|
||||
|
||||
setup(async () => {
|
||||
service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService());
|
||||
testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesmanagementmainservice');
|
||||
untitledWorkspacesHomePath = path.join(testDir, 'Workspaces');
|
||||
|
||||
// Delete any existing backups completely and then re-create it.
|
||||
await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE);
|
||||
environmentService = new class TestEnvironmentService extends EnvironmentMainService {
|
||||
constructor() {
|
||||
super(parseArgs(process.argv, OPTIONS));
|
||||
}
|
||||
get untitledWorkspacesHome(): URI {
|
||||
return URI.file(untitledWorkspacesHomePath);
|
||||
}
|
||||
};
|
||||
|
||||
service = new WorkspacesManagementMainService(environmentService, new NullLogService(), new TestBackupMainService(), new TestDialogMainService());
|
||||
|
||||
return pfs.mkdirp(untitledWorkspacesHomePath);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
return pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE);
|
||||
return pfs.rimraf(testDir);
|
||||
});
|
||||
|
||||
function assertPathEquals(p1: string, p2: string): void {
|
||||
@@ -162,11 +163,11 @@ suite('WorkspacesMainService', () => {
|
||||
p2 = normalizeDriveLetter(p2);
|
||||
}
|
||||
|
||||
assert.equal(p1, p2);
|
||||
assert.strictEqual(p1, p2);
|
||||
}
|
||||
|
||||
function assertEqualURI(u1: URI, u2: URI): void {
|
||||
assert.equal(u1.toString(), u2.toString());
|
||||
assert.strictEqual(u1.toString(), u2.toString());
|
||||
}
|
||||
|
||||
test('createWorkspace (folders)', async () => {
|
||||
@@ -176,7 +177,7 @@ suite('WorkspacesMainService', () => {
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
|
||||
const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 2);
|
||||
assert.strictEqual(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);
|
||||
@@ -190,11 +191,11 @@ suite('WorkspacesMainService', () => {
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
|
||||
const ws = (JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 2);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual((<IRawFileWorkspaceFolder>ws.folders[0]).name, 'currentworkingdirectory');
|
||||
assert.strictEqual((<IRawFileWorkspaceFolder>ws.folders[1]).name, 'tempdir');
|
||||
});
|
||||
|
||||
test('createUntitledWorkspace (folders as other resource URIs)', async () => {
|
||||
@@ -207,12 +208,12 @@ suite('WorkspacesMainService', () => {
|
||||
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.strictEqual(ws.folders.length, 2);
|
||||
assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[0]).uri, folder1URI.toString(true));
|
||||
assert.strictEqual((<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');
|
||||
assert.strictEqual(ws.remoteAuthority, 'server');
|
||||
});
|
||||
|
||||
test('createWorkspaceSync (folders)', () => {
|
||||
@@ -222,7 +223,7 @@ suite('WorkspacesMainService', () => {
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
|
||||
const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace;
|
||||
assert.equal(ws.folders.length, 2);
|
||||
assert.strictEqual(ws.folders.length, 2);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd());
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, os.tmpdir());
|
||||
|
||||
@@ -237,12 +238,12 @@ suite('WorkspacesMainService', () => {
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
|
||||
const ws = JSON.parse(fs.readFileSync(workspace.configPath.fsPath).toString()) as IStoredWorkspace;
|
||||
assert.equal(ws.folders.length, 2);
|
||||
assert.strictEqual(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');
|
||||
assert.strictEqual((<IRawFileWorkspaceFolder>ws.folders[0]).name, 'currentworkingdirectory');
|
||||
assert.strictEqual((<IRawFileWorkspaceFolder>ws.folders[1]).name, 'tempdir');
|
||||
});
|
||||
|
||||
test('createUntitledWorkspaceSync (folders as other resource URIs)', () => {
|
||||
@@ -255,9 +256,9 @@ suite('WorkspacesMainService', () => {
|
||||
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.strictEqual(ws.folders.length, 2);
|
||||
assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[0]).uri, folder1URI.toString(true));
|
||||
assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[1]).uri, folder2URI.toString(true));
|
||||
|
||||
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[0]).name);
|
||||
assert.ok(!(<IRawFileWorkspaceFolder>ws.folders[1]).name);
|
||||
@@ -273,7 +274,7 @@ suite('WorkspacesMainService', () => {
|
||||
workspace.configPath = URI.file(newPath);
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
assert.equal(2, resolved!.folders.length);
|
||||
assert.strictEqual(2, resolved!.folders.length);
|
||||
assertEqualURI(resolved!.configPath, workspace.configPath);
|
||||
assert.ok(resolved!.id);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ something: 'something' })); // invalid workspace
|
||||
@@ -325,39 +326,39 @@ suite('WorkspacesMainService', () => {
|
||||
|
||||
let origConfigPath = URI.file(firstConfigPath);
|
||||
let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace'));
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath);
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
let ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assert.strictEqual(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);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assert.strictEqual(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);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assert.strictEqual(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);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
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));
|
||||
assert.strictEqual(ws.folders.length, 3);
|
||||
assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[0]).uri, URI.file(folder1).toString(true));
|
||||
assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true));
|
||||
assert.strictEqual((<IRawUriWorkspaceFolder>ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true));
|
||||
|
||||
fs.unlinkSync(firstConfigPath);
|
||||
});
|
||||
@@ -369,8 +370,8 @@ suite('WorkspacesMainService', () => {
|
||||
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'));
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
assert.strictEqual(0, newContent.indexOf('// this is a comment'));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
@@ -381,26 +382,22 @@ suite('WorkspacesMainService', () => {
|
||||
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 newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
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();
|
||||
}
|
||||
|
||||
(!isWindows ? test.skip : test)('rewriteWorkspaceFileForNewLocation (unc paths)', async () => {
|
||||
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 folder3Location = path.join(workspaceLocation, '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 newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, true, workspaceConfigPath, extUriBiasedIgnorePathCase);
|
||||
const ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1Location);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, folder2Location);
|
||||
@@ -422,17 +419,15 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('getUntitledWorkspaceSync', async function () {
|
||||
this.retries(3);
|
||||
|
||||
let untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(untitled.length, 0);
|
||||
assert.strictEqual(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);
|
||||
assert.strictEqual(1, untitled.length);
|
||||
assert.strictEqual(untitledOne.id, untitled[0].workspace.id);
|
||||
|
||||
const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]);
|
||||
assert.ok(fs.existsSync(untitledTwo.configPath.fsPath));
|
||||
@@ -444,14 +439,50 @@ suite('WorkspacesMainService', () => {
|
||||
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);
|
||||
assert.strictEqual(2, untitled.length);
|
||||
|
||||
service.deleteUntitledWorkspaceSync(untitledOne);
|
||||
untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(1, untitled.length);
|
||||
assert.strictEqual(1, untitled.length);
|
||||
|
||||
service.deleteUntitledWorkspaceSync(untitledTwo);
|
||||
untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(0, untitled.length);
|
||||
assert.strictEqual(0, untitled.length);
|
||||
});
|
||||
|
||||
test('getSingleWorkspaceIdentifier', async function () {
|
||||
const nonLocalUri = URI.parse('myscheme://server/work/p/f1');
|
||||
const nonLocalUriId = getSingleFolderWorkspaceIdentifier(nonLocalUri);
|
||||
assert.ok(nonLocalUriId?.id);
|
||||
|
||||
const localNonExistingUri = URI.file(path.join(testDir, 'f1'));
|
||||
const localNonExistingUriId = getSingleFolderWorkspaceIdentifier(localNonExistingUri);
|
||||
assert.ok(!localNonExistingUriId);
|
||||
|
||||
fs.mkdirSync(path.join(testDir, 'f1'));
|
||||
|
||||
const localExistingUri = URI.file(path.join(testDir, 'f1'));
|
||||
const localExistingUriId = getSingleFolderWorkspaceIdentifier(localExistingUri);
|
||||
assert.ok(localExistingUriId?.id);
|
||||
});
|
||||
|
||||
test('workspace identifiers are stable', function () {
|
||||
|
||||
// workspace identifier (local)
|
||||
assert.strictEqual(getWorkspaceIdentifier(URI.file('/hello/test')).id, isWindows /* slash vs backslash */ ? '9f3efb614e2cd7924e4b8076e6c72233' : 'e36736311be12ff6d695feefe415b3e8');
|
||||
|
||||
// single folder identifier (local)
|
||||
const fakeStat = {
|
||||
ino: 1611312115129,
|
||||
birthtimeMs: 1611312115129,
|
||||
birthtime: new Date(1611312115129)
|
||||
};
|
||||
assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.file('/hello/test'), fakeStat as fs.Stats)?.id, isWindows /* slash vs backslash */ ? '9a8441e897e5174fa388bc7ef8f7a710' : '1d726b3d516dc2a6d343abf4797eaaef');
|
||||
|
||||
// workspace identifier (remote)
|
||||
assert.strictEqual(getWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test')).id, '786de4f224d57691f218dc7f31ee2ee3');
|
||||
|
||||
// single folder identifier (remote)
|
||||
assert.strictEqual(getSingleFolderWorkspaceIdentifier(URI.parse('vscode-remote:/hello/test'))?.id, '786de4f224d57691f218dc7f31ee2ee3');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user