Merge from vscode 2f984aad710215f4e4684a035bb02f55d1a9e2cc (#9819)

This commit is contained in:
Anthony Dresser
2020-04-01 00:44:39 -07:00
committed by GitHub
parent 0e27aaa61f
commit 0bfbdc62ed
247 changed files with 5402 additions and 3311 deletions

View File

@@ -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>>;
}

View File

@@ -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)
};
}

View File

@@ -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);
});
});
});

View File

@@ -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 { }

View File

@@ -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());

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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[]' },

View File

@@ -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

View File

@@ -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();
});
}

View File

@@ -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 ("/").

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 });
}
}));

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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--) {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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));

View File

@@ -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> } {

View File

@@ -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`);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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> {

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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', () => {

View File

@@ -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: {} },
]);

View File

@@ -15,6 +15,7 @@ export interface IOpenedWindow {
folderUri?: ISingleFolderWorkspaceIdentifier;
title: string;
filename?: string;
dirty: boolean;
}
export interface IBaseOpenWindowsOptions {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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));
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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));