mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 2f984aad710215f4e4684a035bb02f55d1a9e2cc (#9819)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
|
||||
@@ -15,6 +15,12 @@ export interface IWorkspaceBackupInfo {
|
||||
remoteAuthority?: string;
|
||||
}
|
||||
|
||||
export function isWorkspaceBackupInfo(obj: unknown): obj is IWorkspaceBackupInfo {
|
||||
const candidate = obj as IWorkspaceBackupInfo;
|
||||
|
||||
return candidate && isWorkspaceIdentifier(candidate.workspace);
|
||||
}
|
||||
|
||||
export interface IBackupMainService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -31,4 +37,12 @@ export interface IBackupMainService {
|
||||
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
|
||||
unregisterFolderBackupSync(folderUri: URI): void;
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void;
|
||||
|
||||
/**
|
||||
* All folders or workspaces that are known to have
|
||||
* backups stored. This call is long running because
|
||||
* it checks for each backup location if any backups
|
||||
* are stored.
|
||||
*/
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import * as crypto from 'crypto';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -28,9 +27,9 @@ export class BackupMainService implements IBackupMainService {
|
||||
protected backupHome: string;
|
||||
protected workspacesJsonPath: string;
|
||||
|
||||
private rootWorkspaces: IWorkspaceBackupInfo[] = [];
|
||||
private folderWorkspaces: URI[] = [];
|
||||
private emptyWorkspaces: IEmptyWindowBackupInfo[] = [];
|
||||
private workspaces: IWorkspaceBackupInfo[] = [];
|
||||
private folders: URI[] = [];
|
||||
private emptyWindows: IEmptyWindowBackupInfo[] = [];
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@@ -51,31 +50,31 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// read empty workspaces backups first
|
||||
if (backups.emptyWorkspaceInfos) {
|
||||
this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
|
||||
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
|
||||
} else if (Array.isArray(backups.emptyWorkspaces)) {
|
||||
// read legacy entries
|
||||
this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(backupFolder => ({ backupFolder })));
|
||||
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(emptyWindow => ({ backupFolder: emptyWindow })));
|
||||
}
|
||||
|
||||
// read workspace backups
|
||||
let rootWorkspaces: IWorkspaceBackupInfo[] = [];
|
||||
try {
|
||||
if (Array.isArray(backups.rootURIWorkspaces)) {
|
||||
rootWorkspaces = backups.rootURIWorkspaces.map(f => ({ workspace: { id: f.id, configPath: URI.parse(f.configURIPath) }, remoteAuthority: f.remoteAuthority }));
|
||||
rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority }));
|
||||
} else if (Array.isArray(backups.rootWorkspaces)) {
|
||||
rootWorkspaces = backups.rootWorkspaces.map(f => ({ workspace: { id: f.id, configPath: URI.file(f.configPath) } }));
|
||||
rootWorkspaces = backups.rootWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } }));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore URI parsing exceptions
|
||||
}
|
||||
|
||||
this.rootWorkspaces = await this.validateWorkspaces(rootWorkspaces);
|
||||
this.workspaces = await this.validateWorkspaces(rootWorkspaces);
|
||||
|
||||
// read folder backups
|
||||
let workspaceFolders: URI[] = [];
|
||||
try {
|
||||
if (Array.isArray(backups.folderURIWorkspaces)) {
|
||||
workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f));
|
||||
workspaceFolders = backups.folderURIWorkspaces.map(folder => URI.parse(folder));
|
||||
} else if (Array.isArray(backups.folderWorkspaces)) {
|
||||
// migrate legacy folder paths
|
||||
workspaceFolders = [];
|
||||
@@ -93,7 +92,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
// ignore URI parsing exceptions
|
||||
}
|
||||
|
||||
this.folderWorkspaces = await this.validateFolders(workspaceFolders);
|
||||
this.folders = await this.validateFolders(workspaceFolders);
|
||||
|
||||
// save again in case some workspaces or folders have been removed
|
||||
await this.save();
|
||||
@@ -106,7 +105,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.rootWorkspaces.slice(0); // return a copy
|
||||
return this.workspaces.slice(0); // return a copy
|
||||
}
|
||||
|
||||
getFolderBackupPaths(): URI[] {
|
||||
@@ -116,7 +115,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.folderWorkspaces.slice(0); // return a copy
|
||||
return this.folders.slice(0); // return a copy
|
||||
}
|
||||
|
||||
isHotExitEnabled(): boolean {
|
||||
@@ -134,12 +133,12 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
|
||||
return this.emptyWorkspaces.slice(0); // return a copy
|
||||
return this.emptyWindows.slice(0); // return a copy
|
||||
}
|
||||
|
||||
registerWorkspaceBackupSync(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string {
|
||||
if (!this.rootWorkspaces.some(window => workspaceInfo.workspace.id === window.workspace.id)) {
|
||||
this.rootWorkspaces.push(workspaceInfo);
|
||||
if (!this.workspaces.some(workspace => workspaceInfo.workspace.id === workspace.workspace.id)) {
|
||||
this.workspaces.push(workspaceInfo);
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
@@ -188,16 +187,16 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
|
||||
const id = workspace.id;
|
||||
let index = arrays.firstIndex(this.rootWorkspaces, w => w.workspace.id === id);
|
||||
const index = this.workspaces.findIndex(workspace => workspace.workspace.id === id);
|
||||
if (index !== -1) {
|
||||
this.rootWorkspaces.splice(index, 1);
|
||||
this.workspaces.splice(index, 1);
|
||||
this.saveSync();
|
||||
}
|
||||
}
|
||||
|
||||
registerFolderBackupSync(folderUri: URI): string {
|
||||
if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri))) {
|
||||
this.folderWorkspaces.push(folderUri);
|
||||
if (!this.folders.some(folder => areResourcesEquals(folderUri, folder))) {
|
||||
this.folders.push(folderUri);
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
@@ -205,9 +204,9 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
unregisterFolderBackupSync(folderUri: URI): void {
|
||||
let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri));
|
||||
const index = this.folders.findIndex(folder => areResourcesEquals(folderUri, folder));
|
||||
if (index !== -1) {
|
||||
this.folderWorkspaces.splice(index, 1);
|
||||
this.folders.splice(index, 1);
|
||||
this.saveSync();
|
||||
}
|
||||
}
|
||||
@@ -216,8 +215,8 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// Generate a new folder if this is a new empty workspace
|
||||
const backupFolder = backupFolderCandidate || this.getRandomEmptyWindowId();
|
||||
if (!this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, backupFolder, !platform.isLinux))) {
|
||||
this.emptyWorkspaces.push({ backupFolder, remoteAuthority });
|
||||
if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, backupFolder, !platform.isLinux))) {
|
||||
this.emptyWindows.push({ backupFolder, remoteAuthority });
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
@@ -225,9 +224,9 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void {
|
||||
let index = arrays.firstIndex(this.emptyWorkspaces, w => !!w.backupFolder && isEqual(w.backupFolder, backupFolder, !platform.isLinux));
|
||||
const index = this.emptyWindows.findIndex(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, backupFolder, !platform.isLinux));
|
||||
if (index !== -1) {
|
||||
this.emptyWorkspaces.splice(index, 1);
|
||||
this.emptyWindows.splice(index, 1);
|
||||
this.saveSync();
|
||||
}
|
||||
}
|
||||
@@ -255,7 +254,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
seenIds.add(workspace.id);
|
||||
|
||||
const backupPath = this.getBackupPath(workspace.id);
|
||||
const hasBackups = await this.hasBackups(backupPath);
|
||||
const hasBackups = await this.doHasBackups(backupPath);
|
||||
|
||||
// If the workspace has no backups, ignore it
|
||||
if (hasBackups) {
|
||||
@@ -287,7 +286,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
seenIds.add(key);
|
||||
|
||||
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
|
||||
const hasBackups = await this.hasBackups(backupPath);
|
||||
const hasBackups = await this.doHasBackups(backupPath);
|
||||
|
||||
// If the folder has no backups, ignore it
|
||||
if (hasBackups) {
|
||||
@@ -325,7 +324,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
seenIds.add(backupFolder);
|
||||
|
||||
const backupPath = this.getBackupPath(backupFolder);
|
||||
if (await this.hasBackups(backupPath)) {
|
||||
if (await this.doHasBackups(backupPath)) {
|
||||
result.push(backupInfo);
|
||||
} else {
|
||||
await this.deleteStaleBackup(backupPath);
|
||||
@@ -350,7 +349,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// New empty window backup
|
||||
let newBackupFolder = this.getRandomEmptyWindowId();
|
||||
while (this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
newBackupFolder = this.getRandomEmptyWindowId();
|
||||
}
|
||||
|
||||
@@ -362,7 +361,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
|
||||
return false;
|
||||
}
|
||||
this.emptyWorkspaces.push({ backupFolder: newBackupFolder });
|
||||
this.emptyWindows.push({ backupFolder: newBackupFolder });
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -371,7 +370,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// New empty window backup
|
||||
let newBackupFolder = this.getRandomEmptyWindowId();
|
||||
while (this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
newBackupFolder = this.getRandomEmptyWindowId();
|
||||
}
|
||||
|
||||
@@ -383,12 +382,53 @@ export class BackupMainService implements IBackupMainService {
|
||||
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
|
||||
return false;
|
||||
}
|
||||
this.emptyWorkspaces.push({ backupFolder: newBackupFolder });
|
||||
this.emptyWindows.push({ backupFolder: newBackupFolder });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async hasBackups(backupPath: string): Promise<boolean> {
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
|
||||
const dirtyWorkspaces: Array<IWorkspaceIdentifier | URI> = [];
|
||||
|
||||
// Workspaces with backups
|
||||
for (const workspace of this.workspaces) {
|
||||
if ((await this.hasBackups(workspace))) {
|
||||
dirtyWorkspaces.push(workspace.workspace);
|
||||
}
|
||||
}
|
||||
|
||||
// Folders with backups
|
||||
for (const folder of this.folders) {
|
||||
if ((await this.hasBackups(folder))) {
|
||||
dirtyWorkspaces.push(folder);
|
||||
}
|
||||
}
|
||||
|
||||
return dirtyWorkspaces;
|
||||
}
|
||||
|
||||
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise<boolean> {
|
||||
let backupPath: string;
|
||||
|
||||
// Folder
|
||||
if (URI.isUri(backupLocation)) {
|
||||
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
|
||||
}
|
||||
|
||||
// Workspace
|
||||
else if (isWorkspaceBackupInfo(backupLocation)) {
|
||||
backupPath = this.getBackupPath(backupLocation.workspace.id);
|
||||
}
|
||||
|
||||
// Empty
|
||||
else {
|
||||
backupPath = backupLocation.backupFolder;
|
||||
}
|
||||
|
||||
return this.doHasBackups(backupPath);
|
||||
}
|
||||
|
||||
private async doHasBackups(backupPath: string): Promise<boolean> {
|
||||
try {
|
||||
const backupSchemas = await readdir(backupPath);
|
||||
|
||||
@@ -427,10 +467,10 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
private serializeBackups(): IBackupWorkspacesFormat {
|
||||
return {
|
||||
rootURIWorkspaces: this.rootWorkspaces.map(f => ({ id: f.workspace.id, configURIPath: f.workspace.configPath.toString(), remoteAuthority: f.remoteAuthority })),
|
||||
folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()),
|
||||
emptyWorkspaceInfos: this.emptyWorkspaces,
|
||||
emptyWorkspaces: this.emptyWorkspaces.map(info => info.backupFolder)
|
||||
rootURIWorkspaces: this.workspaces.map(workspace => ({ id: workspace.workspace.id, configURIPath: workspace.workspace.configPath.toString(), remoteAuthority: workspace.remoteAuthority })),
|
||||
folderURIWorkspaces: this.folders.map(folder => folder.toString()),
|
||||
emptyWorkspaceInfos: this.emptyWindows,
|
||||
emptyWorkspaces: this.emptyWindows.map(emptyWindow => emptyWindow.backupFolder)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { createHash } from 'crypto';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
suite('BackupMainService', () => {
|
||||
|
||||
@@ -731,4 +732,45 @@ suite('BackupMainService', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('getDirtyWorkspaces', () => {
|
||||
test('should report if a workspace or folder has backups', async () => {
|
||||
const folderBackupPath = service.registerFolderBackupSync(fooFile);
|
||||
|
||||
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
|
||||
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
|
||||
|
||||
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
|
||||
|
||||
try {
|
||||
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
|
||||
await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled));
|
||||
} catch (error) {
|
||||
// ignore - folder might exist already
|
||||
}
|
||||
|
||||
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
|
||||
|
||||
fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), '');
|
||||
fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), '');
|
||||
|
||||
const dirtyWorkspaces = await service.getDirtyWorkspaces();
|
||||
assert.equal(dirtyWorkspaces.length, 2);
|
||||
|
||||
let found = 0;
|
||||
for (const dirtyWorkpspace of dirtyWorkspaces) {
|
||||
if (URI.isUri(dirtyWorkpspace)) {
|
||||
if (isEqual(fooFile, dirtyWorkpspace)) {
|
||||
found++;
|
||||
}
|
||||
} else {
|
||||
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) {
|
||||
found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(found, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,8 +54,7 @@ export class BrowserClipboardService implements IClipboardService {
|
||||
}
|
||||
|
||||
readFindText(): string {
|
||||
// @ts-ignore
|
||||
return undefined;
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
|
||||
writeFindText(text: string): void { }
|
||||
|
||||
@@ -29,7 +29,6 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
||||
fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this._register(fileService.watch(settingsResource));
|
||||
this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService));
|
||||
this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel());
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ export class ElectronMainService implements IElectronMainService {
|
||||
workspace: window.openedWorkspace,
|
||||
folderUri: window.openedFolderUri,
|
||||
title: window.win.getTitle(),
|
||||
filename: window.getRepresentedFilename()
|
||||
filename: window.getRepresentedFilename(),
|
||||
dirty: window.isDocumentEdited()
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -271,7 +272,7 @@ export class ElectronMainService implements IElectronMainService {
|
||||
async setDocumentEdited(windowId: number | undefined, edited: boolean): Promise<void> {
|
||||
const window = this.windowById(windowId);
|
||||
if (window) {
|
||||
window.win.setDocumentEdited(edited);
|
||||
window.setDocumentEdited(edited);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ export interface ParsedArgs {
|
||||
'nolazy'?: boolean;
|
||||
'force-device-scale-factor'?: string;
|
||||
'force-renderer-accessibility'?: boolean;
|
||||
'ignore-certificate-error'?: boolean;
|
||||
'ignore-certificate-errors'?: boolean;
|
||||
'allow-insecure-localhost'?: boolean;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ export interface IEnvironmentService extends IUserHomeProvider {
|
||||
extensionsPath?: string;
|
||||
extensionDevelopmentLocationURI?: URI[];
|
||||
extensionTestsLocationURI?: URI;
|
||||
extensionEnabledProposedApi?: string[] | undefined;
|
||||
logExtensionHostCommunication?: boolean;
|
||||
|
||||
debugExtensionHost: IExtensionHostDebugParams;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as minimist from 'vscode-minimist';
|
||||
import * as minimist from 'minimist';
|
||||
import * as os from 'os';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
@@ -128,7 +128,7 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
||||
'nolazy': { type: 'boolean' }, // node inspect
|
||||
'force-device-scale-factor': { type: 'string' },
|
||||
'force-renderer-accessibility': { type: 'boolean' },
|
||||
'ignore-certificate-error': { type: 'boolean' },
|
||||
'ignore-certificate-errors': { type: 'boolean' },
|
||||
'allow-insecure-localhost': { type: 'boolean' },
|
||||
'_urls': { type: 'string[]' },
|
||||
|
||||
|
||||
@@ -233,6 +233,18 @@ export class EnvironmentService implements IEnvironmentService {
|
||||
return false;
|
||||
}
|
||||
|
||||
get extensionEnabledProposedApi(): string[] | undefined {
|
||||
if (Array.isArray(this.args['enable-proposed-api'])) {
|
||||
return this.args['enable-proposed-api'];
|
||||
}
|
||||
|
||||
if ('enable-proposed-api' in this.args) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this._args, this.isBuilt); }
|
||||
@memoize
|
||||
|
||||
@@ -26,6 +26,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ReadableStreamEvents, transform } from 'vs/base/common/stream';
|
||||
import { createReadStream } from 'vs/platform/files/common/io';
|
||||
import { insert } from 'vs/base/common/arrays';
|
||||
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
@@ -524,7 +525,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
// Add to list of folders to watch recursively
|
||||
const folderToWatch = { path: this.toFilePath(resource), excludes };
|
||||
this.recursiveFoldersToWatch.push(folderToWatch);
|
||||
const remove = insert(this.recursiveFoldersToWatch, folderToWatch);
|
||||
|
||||
// Trigger update
|
||||
this.refreshRecursiveWatchers();
|
||||
@@ -532,7 +533,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
return toDisposable(() => {
|
||||
|
||||
// Remove from list of folders to watch recursively
|
||||
this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1);
|
||||
remove();
|
||||
|
||||
// Trigger update
|
||||
this.refreshRecursiveWatchers();
|
||||
@@ -543,10 +544,8 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
// Buffer requests for recursive watching to decide on right watcher
|
||||
// that supports potentially watching more than one folder at once
|
||||
this.recursiveWatchRequestDelayer.trigger(() => {
|
||||
this.recursiveWatchRequestDelayer.trigger(async () => {
|
||||
this.doRefreshRecursiveWatchers();
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { rtrim, endsWith } from 'vs/base/common/strings';
|
||||
import { rtrim } from 'vs/base/common/strings';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class FileWatcher implements IDisposable {
|
||||
@@ -22,7 +22,7 @@ export class FileWatcher implements IDisposable {
|
||||
) {
|
||||
this.folder = folders[0];
|
||||
|
||||
if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) {
|
||||
if (this.folder.path.indexOf('\\\\') === 0 && this.folder.path.endsWith(posix.sep)) {
|
||||
// for some weird reason, node adds a trailing slash to UNC paths
|
||||
// we never ever want trailing slashes as our base path unless
|
||||
// someone opens root ("/").
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isEqualOrParent, basename } from 'vs/base/common/resources';
|
||||
import { endsWith } from 'vs/base/common/strings';
|
||||
|
||||
export interface ILabelService {
|
||||
_serviceBrand: undefined;
|
||||
@@ -61,7 +60,7 @@ export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, w
|
||||
}
|
||||
|
||||
let filename = basename(workspace.configPath);
|
||||
if (endsWith(filename, WORKSPACE_EXTENSION)) {
|
||||
if (filename.endsWith(WORKSPACE_EXTENSION)) {
|
||||
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
|
||||
}
|
||||
return localize('workspaceName', "{0} (Workspace)", filename);
|
||||
|
||||
@@ -65,7 +65,7 @@ export interface INeverShowAgainOptions {
|
||||
|
||||
/**
|
||||
* Whether to persist the choice in the current workspace or for all workspaces. By
|
||||
* default it will be persisted for all workspaces.
|
||||
* default it will be persisted for all workspaces (= `NeverShowAgainScope.GLOBAL`).
|
||||
*/
|
||||
readonly scope?: NeverShowAgainScope;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
|
||||
disposables.add(picker.onDidAccept(() => {
|
||||
const [item] = picker.selectedItems;
|
||||
if (item) {
|
||||
this.quickInputService.quickAccess.show(item.prefix);
|
||||
this.quickInputService.quickAccess.show(item.prefix, { preserveValue: true });
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -37,7 +37,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
|
||||
disposables.add(picker.onDidChangeValue(value => {
|
||||
const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length));
|
||||
if (providerDescriptor && providerDescriptor.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) {
|
||||
this.quickInputService.quickAccess.show(providerDescriptor.prefix);
|
||||
this.quickInputService.quickAccess.show(providerDescriptor.prefix, { preserveValue: true });
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -11,14 +11,6 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cance
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
interface IInternalQuickAccessOptions extends IQuickAccessOptions {
|
||||
|
||||
/**
|
||||
* Internal option to not rewrite the filter value at all but use it as is.
|
||||
*/
|
||||
preserveFilterValue?: boolean;
|
||||
}
|
||||
|
||||
export class QuickAccessController extends Disposable implements IQuickAccessController {
|
||||
|
||||
private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
|
||||
@@ -39,7 +31,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
super();
|
||||
}
|
||||
|
||||
show(value = '', options?: IInternalQuickAccessOptions): void {
|
||||
show(value = '', options?: IQuickAccessOptions): void {
|
||||
|
||||
// Find provider for the value to show
|
||||
const [provider, descriptor] = this.getOrInstantiateProvider(value);
|
||||
@@ -51,7 +43,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
|
||||
// Apply value only if it is more specific than the prefix
|
||||
// from the provider and we are not instructed to preserve
|
||||
if (value !== descriptor.prefix && !options?.preserveFilterValue) {
|
||||
if (value !== descriptor.prefix && !options?.preserveValue) {
|
||||
visibleQuickAccess.picker.value = value;
|
||||
}
|
||||
|
||||
@@ -62,7 +54,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
}
|
||||
|
||||
// Rewrite the filter value based on certain rules unless disabled
|
||||
if (descriptor && !options?.preserveFilterValue) {
|
||||
if (descriptor && !options?.preserveValue) {
|
||||
let newValue: string | undefined = undefined;
|
||||
|
||||
// If we have a visible provider with a value, take it's filter value but
|
||||
@@ -116,11 +108,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IInternalQuickAccessOptions): void {
|
||||
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void {
|
||||
let valueSelection: [number, number];
|
||||
|
||||
// Preserve: just always put the cursor at the end
|
||||
if (options?.preserveFilterValue) {
|
||||
if (options?.preserveValue) {
|
||||
valueSelection = [picker.value.length, picker.value.length];
|
||||
}
|
||||
|
||||
@@ -147,7 +139,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
disposables.add(picker.onDidChangeValue(value => {
|
||||
const [providerForValue] = this.getOrInstantiateProvider(value);
|
||||
if (providerForValue !== provider) {
|
||||
this.show(value, { preserveFilterValue: true } /* do not rewrite value from user typing! */);
|
||||
this.show(value, { preserveValue: true } /* do not rewrite value from user typing! */);
|
||||
} else {
|
||||
visibleQuickAccess.value = value; // remember the value in our visible one
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import { IQuickPick, IQuickPickItem, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { first, coalesce } from 'vs/base/common/arrays';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ItemActivation } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
|
||||
@@ -22,7 +21,13 @@ export interface IQuickAccessOptions {
|
||||
* Allows to configure a different item activation strategy.
|
||||
* By default the first item in the list will get activated.
|
||||
*/
|
||||
itemActivation?: ItemActivation
|
||||
itemActivation?: ItemActivation;
|
||||
|
||||
/**
|
||||
* Wether to take the input value as is and not restore it
|
||||
* from any existing value if quick access is visible.
|
||||
*/
|
||||
preserveValue?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickAccessController {
|
||||
@@ -177,7 +182,7 @@ export class QuickAccessRegistry implements IQuickAccessRegistry {
|
||||
}
|
||||
|
||||
getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined {
|
||||
const result = prefix ? (first(this.providers, provider => startsWith(prefix, provider.prefix)) || undefined) : undefined;
|
||||
const result = prefix ? (this.providers.find(provider => prefix.startsWith(provider.prefix)) || undefined) : undefined;
|
||||
|
||||
return result || this.defaultProvider;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateR
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { serializableToMap, mapToSerializable } from 'vs/base/common/map';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
|
||||
|
||||
@@ -291,7 +290,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase
|
||||
|
||||
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
|
||||
|
||||
return serializableToMap(JSON.parse(itemsRaw.value.toString()));
|
||||
return new Map(JSON.parse(itemsRaw.value.toString()));
|
||||
}
|
||||
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
@@ -311,7 +310,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase
|
||||
try {
|
||||
this._hasPendingUpdate = true;
|
||||
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items))));
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries()))));
|
||||
|
||||
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
|
||||
} finally {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService';
|
||||
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
|
||||
import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
@@ -117,7 +116,10 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
|
||||
}
|
||||
});
|
||||
|
||||
return { changed: mapToSerializable(changed), deleted: values(deleted) };
|
||||
return {
|
||||
changed: Array.from(changed.entries()),
|
||||
deleted: Array.from(deleted.values())
|
||||
};
|
||||
}
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
@@ -136,7 +138,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
|
||||
// handle call
|
||||
switch (command) {
|
||||
case 'getItems': {
|
||||
return mapToSerializable(this.storageMainService.items);
|
||||
return Array.from(this.storageMainService.items.entries());
|
||||
}
|
||||
|
||||
case 'updateItems': {
|
||||
@@ -182,7 +184,7 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
|
||||
private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void {
|
||||
if (Array.isArray(e.changed) || Array.isArray(e.deleted)) {
|
||||
this._onDidChangeItemsExternal.fire({
|
||||
changed: e.changed ? serializableToMap(e.changed) : undefined,
|
||||
changed: e.changed ? new Map(e.changed) : undefined,
|
||||
deleted: e.deleted ? new Set<string>(e.deleted) : undefined
|
||||
});
|
||||
}
|
||||
@@ -191,18 +193,18 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
const items: Item[] = await this.channel.call('getItems');
|
||||
|
||||
return serializableToMap(items);
|
||||
return new Map(items);
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
const serializableRequest: ISerializableUpdateRequest = Object.create(null);
|
||||
|
||||
if (request.insert) {
|
||||
serializableRequest.insert = mapToSerializable(request.insert);
|
||||
serializableRequest.insert = Array.from(request.insert.entries());
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
serializableRequest.delete = values(request.delete);
|
||||
serializableRequest.delete = Array.from(request.delete.values());
|
||||
}
|
||||
|
||||
return this.channel.call('updateItems', serializableRequest);
|
||||
|
||||
@@ -28,7 +28,7 @@ export const fontStylePattern = '^(\\s*(-?italic|-?bold|-?underline))*\\s*$';
|
||||
|
||||
export interface TokenSelector {
|
||||
match(type: string, modifiers: string[], language: string): number;
|
||||
readonly selectorString: string;
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface TokenTypeOrModifierContribution {
|
||||
@@ -155,7 +155,7 @@ export namespace TokenStylingRule {
|
||||
}
|
||||
export function toJSONObject(rule: TokenStylingRule): any {
|
||||
return {
|
||||
_selector: rule.selector.selectorString,
|
||||
_selector: rule.selector.id,
|
||||
_style: TokenStyle.toJSONObject(rule.style)
|
||||
};
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export namespace TokenStylingRule {
|
||||
return true;
|
||||
}
|
||||
return r1 !== undefined && r2 !== undefined
|
||||
&& r1.selector && r2.selector && r1.selector.selectorString === r2.selector.selectorString
|
||||
&& r1.selector && r2.selector && r1.selector.id === r2.selector.id
|
||||
&& TokenStyle.equals(r1.style, r2.style);
|
||||
}
|
||||
export function is(r: any): r is TokenStylingRule {
|
||||
@@ -203,10 +203,11 @@ export interface ITokenClassificationRegistry {
|
||||
/**
|
||||
* Parses a token selector from a selector string.
|
||||
* @param selectorString selector string in the form (*|type)(.modifier)*
|
||||
* @param language language to which the selector applies or undefined if the selector is for all languafe
|
||||
* @returns the parsesd selector
|
||||
* @throws an error if the string is not a valid selector
|
||||
*/
|
||||
parseTokenSelector(selectorString: string): TokenSelector;
|
||||
parseTokenSelector(selectorString: string, language?: string): TokenSelector;
|
||||
|
||||
/**
|
||||
* Register a TokenStyle default to the registry.
|
||||
@@ -335,13 +336,13 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage);
|
||||
}
|
||||
|
||||
public parseTokenSelector(selectorString: string): TokenSelector {
|
||||
const selector = parseClassifierString(selectorString);
|
||||
public parseTokenSelector(selectorString: string, language?: string): TokenSelector {
|
||||
const selector = parseClassifierString(selectorString, language);
|
||||
|
||||
if (!selector.type) {
|
||||
return {
|
||||
match: () => -1,
|
||||
selectorString
|
||||
id: '$invalid'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,7 +353,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
if (selector.language !== language) {
|
||||
return -1;
|
||||
}
|
||||
score += 100;
|
||||
score += 10;
|
||||
}
|
||||
if (selector.type !== TOKEN_TYPE_WILDCARD) {
|
||||
const hierarchy = this.getTypeHierarchy(type);
|
||||
@@ -370,7 +371,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
}
|
||||
return score + selector.modifiers.length * 100;
|
||||
},
|
||||
selectorString
|
||||
id: `${[selector.type, ...selector.modifiers.sort()].join('.')}${selector.language !== undefined ? ':' + selector.language : ''}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -379,8 +380,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
}
|
||||
|
||||
public deregisterTokenStyleDefault(selector: TokenSelector): void {
|
||||
const selectorString = selector.selectorString;
|
||||
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.selectorString !== selectorString);
|
||||
const selectorString = selector.id;
|
||||
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.id !== selectorString);
|
||||
}
|
||||
|
||||
public deregisterTokenType(id: string): void {
|
||||
@@ -442,9 +443,11 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
const CHAR_LANGUAGE = TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR.charCodeAt(0);
|
||||
const CHAR_MODIFIER = CLASSIFIER_MODIFIER_SEPARATOR.charCodeAt(0);
|
||||
|
||||
export function parseClassifierString(s: string): { type: string, modifiers: string[], language: string | undefined; } {
|
||||
export function parseClassifierString(s: string, defaultLanguage: string): { type: string, modifiers: string[], language: string; };
|
||||
export function parseClassifierString(s: string, defaultLanguage?: string): { type: string, modifiers: string[], language: string | undefined; };
|
||||
export function parseClassifierString(s: string, defaultLanguage: string | undefined): { type: string, modifiers: string[], language: string | undefined; } {
|
||||
let k = s.length;
|
||||
let language: string | undefined = undefined;
|
||||
let language: string | undefined = defaultLanguage;
|
||||
const modifiers = [];
|
||||
|
||||
for (let i = k - 1; i >= 0; i--) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import * as nls from 'vs/nls';
|
||||
import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
@@ -14,6 +13,10 @@ import Severity from 'vs/base/common/severity';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
function uriGetComparisonKey(resource: URI): string {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
class ResourceStackElement {
|
||||
public readonly type = UndoRedoElementType.Resource;
|
||||
public readonly actual: IResourceUndoRedoElement;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
|
||||
import { CancelablePromise } from 'vs/base/common/async';
|
||||
@@ -144,6 +144,16 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async getSyncPreview(): Promise<ISyncPreviewResult> {
|
||||
if (!this.isEnabled()) {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false };
|
||||
}
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
return this.generatePreview(remoteUserData, lastSyncUserData);
|
||||
}
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
|
||||
// current version is not compatible with cloud version
|
||||
@@ -285,15 +295,14 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreviewResult {
|
||||
export interface IFileSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly content: string | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -20,7 +20,7 @@ import { joinPath, dirname, basename } from 'vs/base/common/resources';
|
||||
import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
interface IExtensionsSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
@@ -82,7 +82,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
const remoteExtensions = this.parseExtensions(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions());
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData });
|
||||
await this.apply({
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
});
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -112,7 +116,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData }, true);
|
||||
await this.apply({
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
}, true);
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`);
|
||||
} finally {
|
||||
@@ -163,12 +171,12 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
|
||||
const previewResult = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
const previewResult = await this.generatePreview(remoteUserData, lastSyncUserData);
|
||||
await this.apply(previewResult);
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<ISyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null;
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
@@ -183,22 +191,31 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
|
||||
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());
|
||||
|
||||
return { added, removed, updated, remote, skippedExtensions, remoteUserData, localExtensions, lastSyncUserData };
|
||||
return {
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
skippedExtensions,
|
||||
remoteUserData,
|
||||
localExtensions,
|
||||
lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
};
|
||||
}
|
||||
|
||||
private getIgnoredExtensions() {
|
||||
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
|
||||
}
|
||||
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasChanges = added.length || removed.length || updated.length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
|
||||
}
|
||||
|
||||
if (added.length || removed.length || updated.length) {
|
||||
if (hasLocalChanged) {
|
||||
// back up all disabled or market place extensions
|
||||
const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid);
|
||||
await this.backupLocal(JSON.stringify(backUpExtensions));
|
||||
|
||||
@@ -13,17 +13,18 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
export interface IMergeResult {
|
||||
local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
remote: IStringDictionary<IStorageValue> | null;
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, logService: ILogService): IMergeResult {
|
||||
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
|
||||
if (!remoteStorage) {
|
||||
return { remote: localStorage, local: { added: {}, removed: [], updated: {} } };
|
||||
return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] };
|
||||
}
|
||||
|
||||
const localToRemote = compare(localStorage, remoteStorage);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return { remote: null, local: { added: {}, removed: [], updated: {} } };
|
||||
return { remote: null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
|
||||
}
|
||||
|
||||
const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
@@ -31,17 +32,19 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
|
||||
const local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> } = { added: {}, removed: [], updated: {} };
|
||||
const remote: IStringDictionary<IStorageValue> = objects.deepClone(remoteStorage);
|
||||
const skipped: string[] = [];
|
||||
|
||||
// Added in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
skipped.push(key);
|
||||
logService.info(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
logService.info(`GlobalState: Skipped adding ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
@@ -60,11 +63,12 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
skipped.push(key);
|
||||
logService.info(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
logService.info(`GlobalState: Skipped updating ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
@@ -78,7 +82,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
logService.info(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
local.removed.push(key);
|
||||
@@ -99,6 +103,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
const remoteValue = remote[key];
|
||||
const localValue = localStorage[key];
|
||||
if (localValue.version < remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${localValue.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
remote[key] = localValue;
|
||||
@@ -106,18 +111,36 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
|
||||
// Removed in local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
// do not remove from remote if it is updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const remoteValue = remote[key];
|
||||
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (storageKey && storageKey.version < remoteValue.version) {
|
||||
// do not remove from remote if storage key is not found
|
||||
if (!storageKey) {
|
||||
skipped.push(key);
|
||||
logService.info(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const remoteValue = remote[key];
|
||||
// do not remove from remote if local data version is old
|
||||
if (storageKey.version < remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// add to local if it was skipped before
|
||||
if (previouslySkipped.indexOf(key) !== -1) {
|
||||
local.added[key] = remote[key];
|
||||
continue;
|
||||
}
|
||||
|
||||
delete remote[key];
|
||||
}
|
||||
|
||||
return { local, remote: areSame(remote, remoteStorage) ? null : remote };
|
||||
return { local, remote: areSame(remote, remoteStorage) ? null : remote, skipped };
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -21,16 +21,22 @@ import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
interface IGlobalSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
skippedStorageKeys: string[] | undefined;
|
||||
}
|
||||
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
@@ -51,8 +57,16 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
) {
|
||||
super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(
|
||||
Event.any(
|
||||
/* Locale change */
|
||||
Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)),
|
||||
/* Storage change */
|
||||
Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
|
||||
/* Storage key registered */
|
||||
this.storageKeysSyncRegistryService.onDidChangeStorageKeys
|
||||
)((() => this._onDidChangeLocal.fire()))
|
||||
);
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
@@ -67,14 +81,19 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Started pulling ui state...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
const { local, remote } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), this.logService);
|
||||
await this.apply({ local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData });
|
||||
const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
await this.apply({
|
||||
local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
});
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -101,9 +120,14 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData }, true);
|
||||
await this.apply({
|
||||
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
skippedStorageKeys: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true
|
||||
}, true);
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`);
|
||||
} finally {
|
||||
@@ -153,13 +177,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
const result = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
|
||||
const result = await this.generatePreview(remoteUserData, lastSyncUserData);
|
||||
await this.apply(result);
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
@@ -171,15 +195,17 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
|
||||
}
|
||||
|
||||
const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), this.logService);
|
||||
const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
|
||||
return { local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData };
|
||||
return {
|
||||
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
};
|
||||
}
|
||||
|
||||
private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalSyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
@@ -201,10 +227,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
|
||||
// update last sync
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
|
||||
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const lastSyncContent = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
// Get file content last to get the latest
|
||||
|
||||
@@ -309,14 +309,14 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[]): Promise<IFileSyncPreviewResult> {
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = []): Promise<IFileSyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<IFileSyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = [], token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -17,7 +17,7 @@ import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
interface ISinppetsSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly local: IStringDictionary<IFileContent>;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
@@ -34,7 +34,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
protected readonly version: number = 1;
|
||||
private readonly snippetsFolder: URI;
|
||||
private readonly snippetsPreviewFolder: URI;
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
private syncPreviewResultPromise: CancelablePromise<ISinppetsSyncPreviewResult> | null = null;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@@ -94,8 +94,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
}));
|
||||
await this.apply();
|
||||
}
|
||||
@@ -128,8 +130,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const { added, removed, updated, remote } = merge(localSnippets, null, null);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
}));
|
||||
|
||||
await this.apply(true);
|
||||
@@ -207,7 +211,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
let previewResult = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
previewResult.resolvedConflicts[key] = content || null;
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token));
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.doGeneratePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token));
|
||||
previewResult = await this.syncPreviewResultPromise;
|
||||
this.setConflicts(previewResult.conflicts);
|
||||
if (!this.conflicts.length) {
|
||||
@@ -252,10 +256,9 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
}
|
||||
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents()
|
||||
.then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token)));
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
@@ -274,7 +277,12 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
}
|
||||
|
||||
private async generatePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null>, token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
|
||||
return this.getSnippetsFileContents()
|
||||
.then(local => this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token));
|
||||
}
|
||||
|
||||
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null;
|
||||
@@ -309,7 +317,18 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
}
|
||||
|
||||
return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote, resolvedConflicts };
|
||||
return {
|
||||
remoteUserData, local,
|
||||
lastSyncUserData,
|
||||
added: mergeResult.added,
|
||||
removed: mergeResult.removed,
|
||||
updated: mergeResult.updated,
|
||||
conflicts,
|
||||
remote: mergeResult.remote,
|
||||
resolvedConflicts,
|
||||
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
|
||||
hasRemoteChanged: mergeResult.remote !== null
|
||||
};
|
||||
}
|
||||
|
||||
private async apply(forcePush?: boolean): Promise<void> {
|
||||
@@ -317,15 +336,13 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return;
|
||||
}
|
||||
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData } = await this.syncPreviewResultPromise;
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
|
||||
const hasChanges = Object.keys(added).length || removed.length || Object.keys(updated).length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (Object.keys(added).length || removed.length || Object.keys(updated).length) {
|
||||
if (hasLocalChanged) {
|
||||
// back up all snippets
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
await this.updateLocalSnippets(added, removed, updated, local);
|
||||
|
||||
@@ -254,6 +254,11 @@ export interface ISyncResourceHandle {
|
||||
|
||||
export type Conflict = { remote: URI, local: URI };
|
||||
|
||||
export interface ISyncPreviewResult {
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
}
|
||||
|
||||
export interface IUserDataSynchroniser {
|
||||
|
||||
readonly resource: SyncResource;
|
||||
@@ -268,6 +273,7 @@ export interface IUserDataSynchroniser {
|
||||
sync(ref?: string): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
getSyncPreview(): Promise<ISyncPreviewResult>
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
@@ -217,7 +217,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
if (await this.hasPreviouslySynced()) {
|
||||
return false;
|
||||
}
|
||||
return await this.hasLocalData();
|
||||
if (!(await this.hasLocalData())) {
|
||||
return false;
|
||||
}
|
||||
for (const synchroniser of [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]) {
|
||||
const preview = await synchroniser.getSyncPreview();
|
||||
if (preview.hasLocalChanged || preview.hasRemoteChanged) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/reques
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
@@ -66,7 +65,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
const result = await asJson<{ url: string, created: number }[]>(context) || [];
|
||||
return result.map(({ url, created }) => ({ ref: relativePath(uri, URI.parse(url).with({ scheme: uri.scheme, authority: uri.authority }))!, created: created * 1000 /* Server returns in seconds */ }));
|
||||
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
|
||||
}
|
||||
|
||||
async resolveContent(resource: SyncResource, ref: string): Promise<string | null> {
|
||||
@@ -76,6 +75,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString();
|
||||
const headers: IHeaders = {};
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -25,7 +25,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -37,7 +37,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -50,7 +50,7 @@ suite('GlobalStateMerge', () => {
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const base = { 'b': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -62,7 +62,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -74,7 +74,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = {};
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -86,7 +86,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -98,7 +98,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -110,7 +110,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = {};
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -122,7 +122,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -134,7 +134,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'd' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'd' } });
|
||||
@@ -146,7 +146,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -158,7 +158,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' }, 'c': { version: 1, value: 'c' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -170,7 +170,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -182,7 +182,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -194,7 +194,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'd' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -206,7 +206,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -219,7 +219,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -232,7 +232,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = {};
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -245,7 +245,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -257,7 +257,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -269,7 +269,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -281,7 +281,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [], new NullLogService());
|
||||
const actual = merge(local, remote, local, [], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -293,7 +293,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -305,7 +305,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'c' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -317,7 +317,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 2, value: 'c' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -330,7 +330,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -343,7 +343,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -356,7 +356,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -364,4 +364,17 @@ suite('GlobalStateMerge', () => {
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when a local value is not yet registered', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
@@ -49,6 +49,10 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
this.syncBarrier.open();
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suite('TestSynchronizer', () => {
|
||||
|
||||
@@ -103,17 +103,17 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
/* pull */
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
@@ -143,17 +143,14 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
/* pull */
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
@@ -178,18 +175,18 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
/* sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
@@ -220,21 +217,19 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface IOpenedWindow {
|
||||
folderUri?: ISingleFolderWorkspaceIdentifier;
|
||||
title: string;
|
||||
filename?: string;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
export interface IBaseOpenWindowsOptions {
|
||||
|
||||
@@ -80,6 +80,9 @@ export interface ICodeWindow extends IDisposable {
|
||||
setRepresentedFilename(name: string): void;
|
||||
getRepresentedFilename(): string | undefined;
|
||||
|
||||
setDocumentEdited(edited: boolean): void;
|
||||
isDocumentEdited(): boolean;
|
||||
|
||||
handleTitleDoubleClick(): void;
|
||||
|
||||
updateTouchBar(items: ISerializableCommandAction[][]): void;
|
||||
|
||||
@@ -454,16 +454,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
//
|
||||
// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
|
||||
//
|
||||
let foldersToRestore: URI[] = [];
|
||||
let workspacesToRestore: IWorkspacePathToOpen[] = [];
|
||||
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) {
|
||||
let foldersToRestore = this.backupMainService.getFolderBackupPaths();
|
||||
foldersToOpen.push(...foldersToRestore.map(f => ({ folderUri: f, remoteAuhority: getRemoteAuthority(f) })));
|
||||
|
||||
// collect from workspaces with hot-exit backups and from previous window session
|
||||
workspacesToRestore = [...this.backupMainService.getWorkspaceBackups(), ...this.workspacesMainService.getUntitledWorkspacesSync()];
|
||||
// Untitled workspaces are always restored
|
||||
workspacesToRestore = this.workspacesMainService.getUntitledWorkspacesSync();
|
||||
workspacesToOpen.push(...workspacesToRestore);
|
||||
|
||||
// Empty windows with backups are always restored
|
||||
emptyToRestore.push(...this.backupMainService.getEmptyWindowBackupPaths());
|
||||
} else {
|
||||
emptyToRestore.length = 0;
|
||||
@@ -495,7 +493,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
const usedWindow = usedWindows[i];
|
||||
if (
|
||||
(usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace
|
||||
(usedWindow.openedFolderUri && foldersToRestore.some(uri => isEqual(uri, usedWindow.openedFolderUri))) || // skip over restored folder
|
||||
(usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window
|
||||
) {
|
||||
continue;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
|
||||
import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { extname } from 'vs/base/common/path';
|
||||
import { extname, isAbsolute } from 'vs/base/common/path';
|
||||
import { dirname, resolvePath, isEqualAuthority, isEqualOrParent, relativePath, extname as resourceExtname } from 'vs/base/common/resources';
|
||||
import * as jsonEdit from 'vs/base/common/jsonEdit';
|
||||
import * as json from 'vs/base/common/json';
|
||||
@@ -18,7 +18,7 @@ import { toSlashes } from 'vs/base/common/extpath';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { Event as CommonEvent } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export const WORKSPACE_EXTENSION = 'code-workspace';
|
||||
@@ -31,18 +31,21 @@ export interface IWorkspacesService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// Management
|
||||
// Workspaces Management
|
||||
enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | null>;
|
||||
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
|
||||
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
|
||||
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;
|
||||
|
||||
// History
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void>;
|
||||
// Workspaces History
|
||||
readonly onRecentlyOpenedChange: Event<void>;
|
||||
addRecentlyOpened(recents: IRecent[]): Promise<void>;
|
||||
removeRecentlyOpened(workspaces: URI[]): Promise<void>;
|
||||
clearRecentlyOpened(): Promise<void>;
|
||||
getRecentlyOpened(): Promise<IRecentlyOpened>;
|
||||
|
||||
// Dirty Workspaces
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
|
||||
}
|
||||
|
||||
export interface IRecentlyOpened {
|
||||
@@ -203,21 +206,22 @@ const SLASH = '/';
|
||||
* Undefined is returned if the folderURI and the targetConfigFolderURI don't have the same schema or authority
|
||||
*
|
||||
* @param folderURI a workspace folder
|
||||
* @param forceAbsolute if set, keep the path absolute
|
||||
* @param folderName a workspace name
|
||||
* @param targetConfigFolderURI the folder where the workspace is living in
|
||||
* @param useSlashForPath if set, use forward slashes for file paths on windows
|
||||
*/
|
||||
export function getStoredWorkspaceFolder(folderURI: URI, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder {
|
||||
export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder {
|
||||
|
||||
if (folderURI.scheme !== targetConfigFolderURI.scheme) {
|
||||
return { name: folderName, uri: folderURI.toString(true) };
|
||||
}
|
||||
|
||||
let folderPath: string | undefined;
|
||||
if (isEqualOrParent(folderURI, targetConfigFolderURI)) {
|
||||
// use relative path
|
||||
folderPath = relativePath(targetConfigFolderURI, folderURI) || '.'; // always uses forward slashes
|
||||
if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) {
|
||||
let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined;
|
||||
if (folderPath !== undefined) {
|
||||
if (folderPath.length === 0) {
|
||||
folderPath = '.';
|
||||
} else if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) {
|
||||
// Windows gets special treatment:
|
||||
// - use backslahes unless slash is used by other existing folders
|
||||
folderPath = folderPath.replace(/\//g, '\\');
|
||||
@@ -249,7 +253,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, folderName: string | un
|
||||
* 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, targetConfigPathURI: URI) {
|
||||
export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) {
|
||||
let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents);
|
||||
|
||||
const sourceConfigFolder = dirname(configPathURI);
|
||||
@@ -258,12 +262,17 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string,
|
||||
const rewrittenFolders: IStoredWorkspaceFolder[] = [];
|
||||
const slashForPath = useSlashForPath(storedWorkspace.folders);
|
||||
|
||||
// Rewrite absolute paths to relative paths if the target workspace folder
|
||||
// is a parent of the location of the workspace file itself. Otherwise keep
|
||||
// using absolute paths.
|
||||
for (const folder of storedWorkspace.folders) {
|
||||
let folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri);
|
||||
rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, folder.name, targetConfigFolder, slashForPath));
|
||||
const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri);
|
||||
let absolute;
|
||||
if (isFromUntitledWorkspace) {
|
||||
// if it was an untitled workspace, try to make paths relative
|
||||
absolute = false;
|
||||
} else {
|
||||
// for existing workspaces, preserve whether a path was absolute or relative
|
||||
absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path);
|
||||
}
|
||||
rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath));
|
||||
}
|
||||
|
||||
// Preserve as much of the existing workspace as possible by using jsonEdit
|
||||
|
||||
@@ -9,7 +9,6 @@ import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { app, JumpListCategory } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels';
|
||||
import { IPath } from 'vs/platform/windows/common/windows';
|
||||
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/workspaces/common/workspaces';
|
||||
@@ -24,6 +23,7 @@ import { exists } from 'vs/base/node/pfs';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
|
||||
|
||||
export const IWorkspacesHistoryMainService = createDecorator<IWorkspacesHistoryMainService>('workspacesHistoryMainService');
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface IWorkspacesHistoryMainService {
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void>;
|
||||
|
||||
addRecentlyOpened(recents: IRecent[]): void;
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
|
||||
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened;
|
||||
removeRecentlyOpened(paths: URI[]): void;
|
||||
clearRecentlyOpened(): void;
|
||||
|
||||
@@ -241,20 +241,23 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
this._onRecentlyOpenedChange.fire();
|
||||
}
|
||||
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
|
||||
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened {
|
||||
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
|
||||
const files: IRecentFile[] = [];
|
||||
|
||||
// Add current workspace to beginning if set
|
||||
const currentWorkspace = include?.config?.workspace;
|
||||
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
|
||||
workspaces.push({ workspace: currentWorkspace });
|
||||
}
|
||||
|
||||
const currentFolder = include?.config?.folderUri;
|
||||
if (currentFolder) {
|
||||
workspaces.push({ folderUri: currentFolder });
|
||||
}
|
||||
|
||||
// Add currently files to open to the beginning if any
|
||||
const currentFiles = include?.config?.filesToOpenOrCreate;
|
||||
if (currentFiles) {
|
||||
for (let currentFile of currentFiles) {
|
||||
const fileUri = currentFile.fileUri;
|
||||
@@ -402,14 +405,14 @@ function location(recent: IRecent): URI {
|
||||
return recent.workspace.configPath;
|
||||
}
|
||||
|
||||
function indexOfWorkspace(arr: IRecent[], workspace: IWorkspaceIdentifier): number {
|
||||
return arrays.firstIndex(arr, w => isRecentWorkspace(w) && w.workspace.id === workspace.id);
|
||||
function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number {
|
||||
return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id);
|
||||
}
|
||||
|
||||
function indexOfFolder(arr: IRecent[], folderURI: ISingleFolderWorkspaceIdentifier): number {
|
||||
return arrays.firstIndex(arr, f => isRecentFolder(f) && areResourcesEqual(f.folderUri, folderURI));
|
||||
function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number {
|
||||
return arr.findIndex(folder => isRecentFolder(folder) && areResourcesEqual(folder.folderUri, candidate));
|
||||
}
|
||||
|
||||
function indexOfFile(arr: IRecentFile[], fileURI: URI): number {
|
||||
return arrays.firstIndex(arr, f => areResourcesEqual(f.fileUri, fileURI));
|
||||
function indexOfFile(arr: IRecentFile[], candidate: URI): number {
|
||||
return arr.findIndex(file => areResourcesEqual(file.fileUri, candidate));
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
|
||||
const storedWorkspaceFolder: IStoredWorkspaceFolder[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, folder.name, untitledWorkspaceConfigFolder));
|
||||
storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
|
||||
export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspacesService, Promise<unknown> /* only methods, not events */, number /* window ID */> {
|
||||
|
||||
@@ -17,7 +18,8 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
constructor(
|
||||
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService
|
||||
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
|
||||
@IBackupMainService private readonly backupMainService: IBackupMainService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -51,12 +53,7 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange;
|
||||
|
||||
async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> {
|
||||
const window = this.windowsMainService.getWindowById(windowId);
|
||||
if (window?.config) {
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened(window.config.workspace, window.config.folderUri, window.config.filesToOpenOrCreate);
|
||||
}
|
||||
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened();
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId));
|
||||
}
|
||||
|
||||
async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise<void> {
|
||||
@@ -72,4 +69,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Dirty Workspaces
|
||||
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
|
||||
return this.backupMainService.getDirtyWorkspaces();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
|
||||
import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
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';
|
||||
@@ -56,6 +56,7 @@ export class TestDialogMainService implements IDialogMainService {
|
||||
}
|
||||
|
||||
export class TestBackupMainService implements IBackupMainService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
isHotExitEnabled(): boolean {
|
||||
@@ -97,6 +98,10 @@ export class TestBackupMainService implements IBackupMainService {
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
suite('WorkspacesMainService', () => {
|
||||
@@ -109,11 +114,27 @@ suite('WorkspacesMainService', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function createWorkspace(folders: string[], names?: string[]) {
|
||||
function createUntitledWorkspace(folders: string[], names?: string[]) {
|
||||
return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
|
||||
}
|
||||
|
||||
function createWorkspaceSync(folders: string[], names?: string[]) {
|
||||
function createWorkspace(workspaceConfigPath: string, folders: (string | URI)[], names?: string[]): void {
|
||||
|
||||
const ws: IStoredWorkspace = {
|
||||
folders: []
|
||||
};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const f = folders[i];
|
||||
const s: IStoredWorkspaceFolder = f instanceof URI ? { uri: f.toString() } : { path: f };
|
||||
if (names) {
|
||||
s.name = names[i];
|
||||
}
|
||||
ws.folders.push(s);
|
||||
}
|
||||
fs.writeFileSync(workspaceConfigPath, JSON.stringify(ws));
|
||||
}
|
||||
|
||||
function createUntitledWorkspaceSync(folders: string[], names?: string[]) {
|
||||
return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
|
||||
}
|
||||
|
||||
@@ -149,7 +170,7 @@ suite('WorkspacesMainService', () => {
|
||||
}
|
||||
|
||||
test('createWorkspace (folders)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -163,7 +184,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('createWorkspace (folders with name)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -195,7 +216,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('createWorkspaceSync (folders)', () => {
|
||||
const workspace = createWorkspaceSync([process.cwd(), os.tmpdir()]);
|
||||
const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -210,7 +231,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('createWorkspaceSync (folders with names)', () => {
|
||||
const workspace = createWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -243,7 +264,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath));
|
||||
|
||||
// make it a valid workspace path
|
||||
@@ -262,7 +283,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support relative paths)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] }));
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -270,7 +291,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support relative paths #2)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] }));
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -278,7 +299,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support relative paths #3)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] }));
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -286,7 +307,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -296,14 +317,15 @@ suite('WorkspacesMainService', () => {
|
||||
test('rewriteWorkspaceFileForNewLocation', async () => {
|
||||
const folder1 = process.cwd(); // absolute path because outside of tmpDir
|
||||
const tmpDir = os.tmpdir();
|
||||
const tmpInsideDir = path.join(os.tmpdir(), 'inside');
|
||||
const tmpInsideDir = path.join(tmpDir, 'inside');
|
||||
|
||||
const workspace = await createWorkspace([folder1, tmpInsideDir, path.join(tmpInsideDir, 'somefolder')]);
|
||||
const origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
const firstConfigPath = path.join(tmpDir, 'myworkspace0.code-workspace');
|
||||
createWorkspace(firstConfigPath, [folder1, 'inside', path.join('inside', 'somefolder')]);
|
||||
const origContent = fs.readFileSync(firstConfigPath).toString();
|
||||
|
||||
let origConfigPath = workspace.configPath;
|
||||
let origConfigPath = URI.file(firstConfigPath);
|
||||
let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace'));
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, workspaceConfigPath);
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath);
|
||||
let ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1); // absolute path because outside of tmpdir
|
||||
@@ -312,7 +334,7 @@ suite('WorkspacesMainService', () => {
|
||||
|
||||
origConfigPath = workspaceConfigPath;
|
||||
workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace'));
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
|
||||
@@ -321,51 +343,51 @@ suite('WorkspacesMainService', () => {
|
||||
|
||||
origConfigPath = workspaceConfigPath;
|
||||
workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace'));
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, tmpInsideDir);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, path.join(tmpInsideDir, 'somefolder'));
|
||||
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, workspaceConfigPath);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assert.equal((<IRawUriWorkspaceFolder>ws.folders[0]).uri, URI.file(folder1).toString(true));
|
||||
assert.equal((<IRawUriWorkspaceFolder>ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true));
|
||||
assert.equal((<IRawUriWorkspaceFolder>ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true));
|
||||
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
fs.unlinkSync(firstConfigPath);
|
||||
});
|
||||
|
||||
test('rewriteWorkspaceFileForNewLocation (preserves comments)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
|
||||
|
||||
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
origContent = `// this is a comment\n${origContent}`;
|
||||
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
|
||||
assert.equal(0, newContent.indexOf('// this is a comment'));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
|
||||
|
||||
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash
|
||||
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
|
||||
const ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.ok(ws.folders.every(f => (<IRawFileWorkspaceFolder>f).path.indexOf('\\') < 0));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
test('rewriteWorkspaceFileForNewLocation (unc paths)', async () => {
|
||||
test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => {
|
||||
if (!isWindows) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -375,10 +397,10 @@ suite('WorkspacesMainService', () => {
|
||||
const folder2Location = '\\\\server\\share2\\some\\path';
|
||||
const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more');
|
||||
|
||||
const workspace = await createWorkspace([folder1Location, folder2Location, folder3Location]);
|
||||
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, workspaceConfigPath);
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
|
||||
const ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1Location);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, folder2Location);
|
||||
@@ -388,14 +410,14 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('deleteUntitledWorkspaceSync (untitled)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
assert.ok(!fs.existsSync(workspace.configPath.fsPath));
|
||||
});
|
||||
|
||||
test('deleteUntitledWorkspaceSync (saved)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
@@ -405,14 +427,14 @@ suite('WorkspacesMainService', () => {
|
||||
let untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(untitled.length, 0);
|
||||
|
||||
const untitledOne = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(fs.existsSync(untitledOne.configPath.fsPath));
|
||||
|
||||
untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(1, untitled.length);
|
||||
assert.equal(untitledOne.id, untitled[0].workspace.id);
|
||||
|
||||
const untitledTwo = await createWorkspace([os.tmpdir(), process.cwd()]);
|
||||
const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]);
|
||||
assert.ok(fs.existsSync(untitledTwo.configPath.fsPath));
|
||||
assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`);
|
||||
const untitledHome = dirname(dirname(untitledTwo.configPath));
|
||||
|
||||
Reference in New Issue
Block a user