Merge from vscode 9bc92b48d945144abb405b9e8df05e18accb9148

This commit is contained in:
ADS Merger
2020-02-19 03:11:35 +00:00
parent 98584d32a7
commit 1e308639e5
253 changed files with 6414 additions and 2296 deletions

View File

@@ -353,7 +353,7 @@ export class UserSettings extends Disposable {
super();
this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes);
this._register(this.fileService.watch(dirname(this.userSettingsResource)));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire()));
}
async loadConfiguration(): Promise<ConfigurationModel> {

View File

@@ -9,7 +9,7 @@ import * as path from 'vs/base/common/path';
import * as fs from 'fs';
import { Registry } from 'vs/platform/registry/common/platform';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import * as uuid from 'vs/base/common/uuid';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { testFile } from 'vs/base/test/node/utils';

View File

@@ -55,7 +55,7 @@ export class FileService extends Disposable implements IFileService {
// Forward events from provider
const providerDisposables = new DisposableStore();
providerDisposables.add(provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes))));
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes))));
providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme })));
if (typeof provider.onDidErrorOccur === 'function') {
providerDisposables.add(provider.onDidErrorOccur(error => this._onError.fire(new Error(error))));
@@ -147,11 +147,11 @@ export class FileService extends Disposable implements IFileService {
//#endregion
private _onAfterOperation: Emitter<FileOperationEvent> = this._register(new Emitter<FileOperationEvent>());
readonly onAfterOperation: Event<FileOperationEvent> = this._onAfterOperation.event;
private _onDidRunOperation = this._register(new Emitter<FileOperationEvent>());
readonly onDidRunOperation = this._onDidRunOperation.event;
private _onError: Emitter<Error> = this._register(new Emitter<Error>());
readonly onError: Event<Error> = this._onError.event;
private _onError = this._register(new Emitter<Error>());
readonly onError = this._onError.event;
//#region File Metadata Resolving
@@ -299,7 +299,7 @@ export class FileService extends Disposable implements IFileService {
const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);
// events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
return fileStat;
}
@@ -549,7 +549,7 @@ export class FileService extends Disposable implements IFileService {
// resolve and send events
const fileStat = await this.resolve(target, { resolveMetadata: true });
this._onAfterOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat));
this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat));
return fileStat;
}
@@ -563,7 +563,7 @@ export class FileService extends Disposable implements IFileService {
// resolve and send events
const fileStat = await this.resolve(target, { resolveMetadata: true });
this._onAfterOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat));
this._onDidRunOperation.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat));
return fileStat;
}
@@ -717,7 +717,7 @@ export class FileService extends Disposable implements IFileService {
// events
const fileStat = await this.resolve(resource, { resolveMetadata: true });
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
return fileStat;
}
@@ -799,15 +799,15 @@ export class FileService extends Disposable implements IFileService {
await provider.delete(resource, { recursive, useTrash });
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
this._onDidRunOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
}
//#endregion
//#region File Watching
private _onFileChanges: Emitter<FileChangesEvent> = this._register(new Emitter<FileChangesEvent>());
readonly onFileChanges: Event<FileChangesEvent> = this._onFileChanges.event;
private _onDidFilesChange = this._register(new Emitter<FileChangesEvent>());
readonly onDidFilesChange = this._onDidFilesChange.event;
private activeWatchers = new Map<string, { disposable: IDisposable, count: number }>();

View File

@@ -63,12 +63,12 @@ export interface IFileService {
* Allows to listen for file changes. The event will fire for every file within the opened workspace
* (if any) as well as all files that have been watched explicitly using the #watch() API.
*/
readonly onFileChanges: Event<FileChangesEvent>;
readonly onDidFilesChange: Event<FileChangesEvent>;
/**
* An event that is fired upon successful completion of a certain file operation.
*/
readonly onAfterOperation: Event<FileOperationEvent>;
readonly onDidRunOperation: Event<FileOperationEvent>;
/**
* Resolve the properties of a file/folder identified by the resource.
@@ -471,15 +471,7 @@ export interface IFileChange {
export class FileChangesEvent {
private readonly _changes: readonly IFileChange[];
constructor(changes: readonly IFileChange[]) {
this._changes = changes;
}
get changes() {
return this._changes;
}
constructor(public readonly changes: readonly IFileChange[]) { }
/**
* Returns true if this change event contains the provided file with the given change type (if provided). In case of
@@ -493,7 +485,7 @@ export class FileChangesEvent {
const checkForChangeType = !isUndefinedOrNull(type);
return this._changes.some(change => {
return this.changes.some(change => {
if (checkForChangeType && change.type !== type) {
return false;
}
@@ -550,11 +542,11 @@ export class FileChangesEvent {
}
private getOfType(type: FileChangeType): IFileChange[] {
return this._changes.filter(change => change.type === type);
return this.changes.filter(change => change.type === type);
}
private hasType(type: FileChangeType): boolean {
return this._changes.some(change => {
return this.changes.some(change => {
return change.type === type;
});
}

View File

@@ -0,0 +1,228 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as resources from 'vs/base/common/resources';
import { FileChangeType, FileType, IWatchOptions, IStat, FileSystemProviderErrorCode, FileSystemProviderError, FileWriteOptions, IFileChange, FileDeleteOptions, FileSystemProviderCapabilities, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
class File implements IStat {
type: FileType;
ctime: number;
mtime: number;
size: number;
name: string;
data?: Uint8Array;
constructor(name: string) {
this.type = FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
}
}
class Directory implements IStat {
type: FileType;
ctime: number;
mtime: number;
size: number;
name: string;
entries: Map<string, File | Directory>;
constructor(name: string) {
this.type = FileType.Directory;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
this.entries = new Map();
}
}
export type Entry = File | Directory;
export class InMemoryFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite;
readonly onDidChangeCapabilities: Event<void> = Event.None;
root = new Directory('');
// --- manage file metadata
async stat(resource: URI): Promise<IStat> {
return this._lookup(resource, false);
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const entry = this._lookupAsDirectory(resource, false);
let result: [string, FileType][] = [];
entry.entries.forEach((child, name) => result.push([name, child.type]));
return result;
}
// --- manage file contents
async readFile(resource: URI): Promise<Uint8Array> {
const data = this._lookupAsFile(resource, false).data;
if (data) {
return data;
}
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let basename = resources.basename(resource);
let parent = this._lookupParentDirectory(resource);
let entry = parent.entries.get(basename);
if (entry instanceof Directory) {
throw new FileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
}
if (!entry && !opts.create) {
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
if (entry && opts.create && !opts.overwrite) {
throw new FileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
}
if (!entry) {
entry = new File(basename);
parent.entries.set(basename, entry);
this._fireSoon({ type: FileChangeType.ADDED, resource });
}
entry.mtime = Date.now();
entry.size = content.byteLength;
entry.data = content;
this._fireSoon({ type: FileChangeType.UPDATED, resource });
}
// --- manage files/folders
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
if (!opts.overwrite && this._lookup(to, true)) {
throw new FileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
}
let entry = this._lookup(from, false);
let oldParent = this._lookupParentDirectory(from);
let newParent = this._lookupParentDirectory(to);
let newName = resources.basename(to);
oldParent.entries.delete(entry.name);
entry.name = newName;
newParent.entries.set(newName, entry);
this._fireSoon(
{ type: FileChangeType.DELETED, resource: from },
{ type: FileChangeType.ADDED, resource: to }
);
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
let dirname = resources.dirname(resource);
let basename = resources.basename(resource);
let parent = this._lookupAsDirectory(dirname, false);
if (parent.entries.has(basename)) {
parent.entries.delete(basename);
parent.mtime = Date.now();
parent.size -= 1;
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { resource, type: FileChangeType.DELETED });
}
}
async mkdir(resource: URI): Promise<void> {
let basename = resources.basename(resource);
let dirname = resources.dirname(resource);
let parent = this._lookupAsDirectory(dirname, false);
let entry = new Directory(basename);
parent.entries.set(entry.name, entry);
parent.mtime = Date.now();
parent.size += 1;
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { type: FileChangeType.ADDED, resource });
}
// --- lookup
private _lookup(uri: URI, silent: false): Entry;
private _lookup(uri: URI, silent: boolean): Entry | undefined;
private _lookup(uri: URI, silent: boolean): Entry | undefined {
let parts = uri.path.split('/');
let entry: Entry = this.root;
for (const part of parts) {
if (!part) {
continue;
}
let child: Entry | undefined;
if (entry instanceof Directory) {
child = entry.entries.get(part);
}
if (!child) {
if (!silent) {
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
} else {
return undefined;
}
}
entry = child;
}
return entry;
}
private _lookupAsDirectory(uri: URI, silent: boolean): Directory {
let entry = this._lookup(uri, silent);
if (entry instanceof Directory) {
return entry;
}
throw new FileSystemProviderError('file not a directory', FileSystemProviderErrorCode.FileNotADirectory);
}
private _lookupAsFile(uri: URI, silent: boolean): File {
let entry = this._lookup(uri, silent);
if (entry instanceof File) {
return entry;
}
throw new FileSystemProviderError('file is a directory', FileSystemProviderErrorCode.FileIsADirectory);
}
private _lookupParentDirectory(uri: URI): Directory {
const dirname = resources.dirname(uri);
return this._lookupAsDirectory(dirname, false);
}
// --- manage file events
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private _bufferedChanges: IFileChange[] = [];
private _fireSoonHandle?: any;
watch(resource: URI, opts: IWatchOptions): IDisposable {
// ignore, fires for all changes...
return Disposable.None;
}
private _fireSoon(...changes: IFileChange[]): void {
this._bufferedChanges.push(...changes);
if (this._fireSoonHandle) {
clearTimeout(this._fireSoonHandle);
}
this._fireSoonHandle = setTimeout(() => {
this._onDidChangeFile.fire(this._bufferedChanges);
this._bufferedChanges.length = 0;
}, 5);
}
}

View File

@@ -20,7 +20,7 @@ export class FileWatcher extends Disposable {
constructor(
private path: string,
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
@@ -101,7 +101,7 @@ export class FileWatcher extends Disposable {
// Fire
if (normalizedFileChanges.length > 0) {
this.onFileChanges(normalizedFileChanges);
this.onDidFilesChange(normalizedFileChanges);
}
return Promise.resolve();

View File

@@ -21,7 +21,7 @@ export class FileWatcher extends Disposable {
constructor(
private folders: IWatcherRequest[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
) {
@@ -68,7 +68,7 @@ export class FileWatcher extends Disposable {
this.service.setVerboseLogging(this.verboseLogging);
const options = {};
this._register(this.service.watch(options)(e => !this.isDisposed && this.onFileChanges(e)));
this._register(this.service.watch(options)(e => !this.isDisposed && this.onDidFilesChange(e)));
this._register(this.service.onLogMessage(m => this.onLogMessage(m)));

View File

@@ -20,7 +20,7 @@ export class FileWatcher extends Disposable {
constructor(
private folders: IWatcherRequest[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
private watcherOptions: IWatcherOptions = {}
@@ -67,7 +67,7 @@ export class FileWatcher extends Disposable {
this.service.setVerboseLogging(this.verboseLogging);
this._register(this.service.watch(this.watcherOptions)(e => !this.isDisposed && this.onFileChanges(e)));
this._register(this.service.watch(this.watcherOptions)(e => !this.isDisposed && this.onDidFilesChange(e)));
this._register(this.service.onLogMessage(m => this.onLogMessage(m)));

View File

@@ -16,7 +16,7 @@ export class FileWatcher implements IDisposable {
constructor(
folders: { path: string, excludes: string[] }[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
@@ -62,7 +62,7 @@ export class FileWatcher implements IDisposable {
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(events);
this.onDidFilesChange(events);
}
}
@@ -72,4 +72,4 @@ export class FileWatcher implements IDisposable {
this.service = undefined;
}
}
}
}

View File

@@ -165,7 +165,7 @@ suite('Disk File Service', function () {
test('createFolder', async () => {
let event: FileOperationEvent | undefined;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const parent = await service.resolve(URI.file(testDir));
@@ -185,7 +185,7 @@ suite('Disk File Service', function () {
test('createFolder: creating multiple folders at once', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const multiFolderPaths = ['a', 'couple', 'of', 'folders'];
const parent = await service.resolve(URI.file(testDir));
@@ -460,7 +460,7 @@ suite('Disk File Service', function () {
test('deleteFile', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const resource = URI.file(join(testDir, 'deep', 'conway.js'));
const source = await service.resolve(resource);
@@ -496,7 +496,7 @@ suite('Disk File Service', function () {
const source = await service.resolve(link);
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
await service.del(source.resource);
@@ -519,7 +519,7 @@ suite('Disk File Service', function () {
await symlink(target.fsPath, link.fsPath);
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
await service.del(link);
@@ -532,7 +532,7 @@ suite('Disk File Service', function () {
test('deleteFolder (recursive)', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const resource = URI.file(join(testDir, 'deep'));
const source = await service.resolve(resource);
@@ -561,7 +561,7 @@ suite('Disk File Service', function () {
test('move', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = URI.file(join(testDir, 'index.html'));
const sourceContents = readFileSync(source.fsPath);
@@ -641,7 +641,7 @@ suite('Disk File Service', function () {
async function testMoveAcrossProviders(sourceFile = 'index.html'): Promise<void> {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = URI.file(join(testDir, sourceFile));
const sourceContents = readFileSync(source.fsPath);
@@ -665,7 +665,7 @@ suite('Disk File Service', function () {
test('move - multi folder', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const multiFolderPaths = ['a', 'couple', 'of', 'folders'];
const renameToPath = join(...multiFolderPaths, 'other.html');
@@ -684,7 +684,7 @@ suite('Disk File Service', function () {
test('move - directory', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = URI.file(join(testDir, 'deep'));
@@ -728,7 +728,7 @@ suite('Disk File Service', function () {
async function testMoveFolderAcrossProviders(): Promise<void> {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = URI.file(join(testDir, 'deep'));
const sourceChildren = readdirSync(source.fsPath);
@@ -753,7 +753,7 @@ suite('Disk File Service', function () {
test('move - MIX CASE', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
assert.ok(source.size > 0);
@@ -774,7 +774,7 @@ suite('Disk File Service', function () {
test('move - same file', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
assert.ok(source.size > 0);
@@ -794,7 +794,7 @@ suite('Disk File Service', function () {
test('move - same file #2', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
assert.ok(source.size > 0);
@@ -817,7 +817,7 @@ suite('Disk File Service', function () {
test('move - source parent of target', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
let source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
const originalSize = source.size;
@@ -839,7 +839,7 @@ suite('Disk File Service', function () {
test('move - FILE_MOVE_CONFLICT', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
let source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
const originalSize = source.size;
@@ -863,7 +863,7 @@ suite('Disk File Service', function () {
let createEvent: FileOperationEvent;
let moveEvent: FileOperationEvent;
let deleteEvent: FileOperationEvent;
disposables.add(service.onAfterOperation(e => {
disposables.add(service.onDidRunOperation(e => {
if (e.operation === FileOperation.CREATE) {
createEvent = e;
} else if (e.operation === FileOperation.DELETE) {
@@ -927,7 +927,7 @@ suite('Disk File Service', function () {
async function doTestCopy(sourceName: string = 'index.html') {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = await service.resolve(URI.file(join(testDir, sourceName)));
const target = URI.file(join(testDir, 'other.html'));
@@ -952,7 +952,7 @@ suite('Disk File Service', function () {
let createEvent: FileOperationEvent;
let copyEvent: FileOperationEvent;
let deleteEvent: FileOperationEvent;
disposables.add(service.onAfterOperation(e => {
disposables.add(service.onDidRunOperation(e => {
if (e.operation === FileOperation.CREATE) {
createEvent = e;
} else if (e.operation === FileOperation.DELETE) {
@@ -1057,7 +1057,7 @@ suite('Disk File Service', function () {
test('copy - same file', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
assert.ok(source.size > 0);
@@ -1077,7 +1077,7 @@ suite('Disk File Service', function () {
test('copy - same file #2', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true });
assert.ok(source.size > 0);
@@ -1567,7 +1567,7 @@ suite('Disk File Service', function () {
async function assertCreateFile(converter: (content: string) => VSBuffer | VSBufferReadable | VSBufferReadableStream): Promise<void> {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const contents = 'Hello World';
const resource = URI.file(join(testDir, 'test.txt'));
@@ -1600,7 +1600,7 @@ suite('Disk File Service', function () {
test('createFile (allows to overwrite existing)', async () => {
let event: FileOperationEvent;
disposables.add(service.onAfterOperation(e => event = e));
disposables.add(service.onDidRunOperation(e => event = e));
const contents = 'Hello World';
const resource = URI.file(join(testDir, 'test.txt'));
@@ -2152,7 +2152,7 @@ suite('Disk File Service', function () {
return event.changes.map(change => `Change: type ${toString(change.type)} path ${change.resource.toString()}`).join('\n');
}
const listenerDisposable = service.onFileChanges(event => {
const listenerDisposable = service.onDidFilesChange(event => {
watcherDisposable.dispose();
listenerDisposable.dispose();

View File

@@ -15,14 +15,14 @@ function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
}
class TestFileWatcher {
private readonly _onFileChanges: Emitter<FileChangesEvent>;
private readonly _onDidFilesChange: Emitter<FileChangesEvent>;
constructor() {
this._onFileChanges = new Emitter<FileChangesEvent>();
this._onDidFilesChange = new Emitter<FileChangesEvent>();
}
get onFileChanges(): Event<FileChangesEvent> {
return this._onFileChanges.event;
get onDidFilesChange(): Event<FileChangesEvent> {
return this._onDidFilesChange.event;
}
report(changes: IDiskFileChange[]): void {
@@ -36,7 +36,7 @@ class TestFileWatcher {
// Emit through event emitter
if (normalizedEvents.length > 0) {
this._onFileChanges.fire(toFileChangesEvent(normalizedEvents));
this._onDidFilesChange.fire(toFileChangesEvent(normalizedEvents));
}
}
}
@@ -62,7 +62,7 @@ suite('Normalizer', () => {
{ path: deleted.fsPath, type: FileChangeType.DELETED },
];
watch.onFileChanges(e => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 3);
assert.ok(e.contains(added, FileChangeType.ADDED));
@@ -101,7 +101,7 @@ suite('Normalizer', () => {
{ path: updatedFile.fsPath, type: FileChangeType.UPDATED }
];
watch.onFileChanges(e => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 5);
@@ -131,7 +131,7 @@ suite('Normalizer', () => {
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onFileChanges(e => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 1);
@@ -156,7 +156,7 @@ suite('Normalizer', () => {
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onFileChanges(e => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
@@ -182,7 +182,7 @@ suite('Normalizer', () => {
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onFileChanges(e => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
@@ -211,7 +211,7 @@ suite('Normalizer', () => {
{ path: updated.fsPath, type: FileChangeType.DELETED }
];
watch.onFileChanges(e => {
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);

View File

@@ -13,6 +13,8 @@ interface IServiceMock<T> {
service: any;
}
const isSinonSpyLike = (fn: Function): fn is sinon.SinonSpy => fn && 'callCount' in fn;
export class TestInstantiationService extends InstantiationService {
private _servciesMap: Map<ServiceIdentifier<any>, any>;
@@ -37,10 +39,10 @@ export class TestInstantiationService extends InstantiationService {
public stub<T>(service: ServiceIdentifier<T>, ctor: Function): T;
public stub<T>(service: ServiceIdentifier<T>, obj: Partial<T>): T;
public stub<T>(service: ServiceIdentifier<T>, ctor: Function, property: string, value: any): sinon.SinonStub;
public stub<T>(service: ServiceIdentifier<T>, obj: Partial<T>, property: string, value: any): sinon.SinonStub;
public stub<T>(service: ServiceIdentifier<T>, property: string, value: any): sinon.SinonStub;
public stub<T>(serviceIdentifier: ServiceIdentifier<T>, arg2: any, arg3?: string, arg4?: any): sinon.SinonStub {
public stub<T, V>(service: ServiceIdentifier<T>, ctor: Function, property: string, value: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
public stub<T, V>(service: ServiceIdentifier<T>, obj: Partial<T>, property: string, value: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
public stub<T, V>(service: ServiceIdentifier<T>, property: string, value: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
public stub<T>(serviceIdentifier: ServiceIdentifier<T>, arg2: any, arg3?: string, arg4?: any): sinon.SinonStub | sinon.SinonSpy {
let service = typeof arg2 !== 'string' ? arg2 : undefined;
let serviceMock: IServiceMock<any> = { id: serviceIdentifier, service: service };
let property = typeof arg2 === 'string' ? arg2 : arg3;
@@ -53,9 +55,11 @@ export class TestInstantiationService extends InstantiationService {
stubObject[property].restore();
}
if (typeof value === 'function') {
stubObject[property] = value;
const spy = isSinonSpyLike(value) ? value : sinon.spy(value);
stubObject[property] = spy;
return spy;
} else {
let stub = value ? sinon.stub().returns(value) : sinon.stub();
const stub = value ? sinon.stub().returns(value) : sinon.stub();
stubObject[property] = stub;
return stub;
}
@@ -67,9 +71,9 @@ export class TestInstantiationService extends InstantiationService {
}
public stubPromise<T>(service?: ServiceIdentifier<T>, fnProperty?: string, value?: any): T | sinon.SinonStub;
public stubPromise<T>(service?: ServiceIdentifier<T>, ctor?: any, fnProperty?: string, value?: any): sinon.SinonStub;
public stubPromise<T>(service?: ServiceIdentifier<T>, obj?: any, fnProperty?: string, value?: any): sinon.SinonStub;
public stubPromise(arg1?: any, arg2?: any, arg3?: any, arg4?: any): sinon.SinonStub {
public stubPromise<T, V>(service?: ServiceIdentifier<T>, ctor?: any, fnProperty?: string, value?: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
public stubPromise<T, V>(service?: ServiceIdentifier<T>, obj?: any, fnProperty?: string, value?: V): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
public stubPromise(arg1?: any, arg2?: any, arg3?: any, arg4?: any): sinon.SinonStub | sinon.SinonSpy {
arg3 = typeof arg2 === 'string' ? Promise.resolve(arg3) : arg3;
arg4 = typeof arg2 !== 'string' && typeof arg3 === 'string' ? Promise.resolve(arg4) : arg4;
return this.stub(arg1, arg2, arg3, arg4);
@@ -124,4 +128,4 @@ export class TestInstantiationService extends InstantiationService {
interface SinonOptions {
mock?: boolean;
stub?: boolean;
}
}

View File

@@ -135,15 +135,15 @@ export namespace IMarkerData {
export function makeKeyOptionalMessage(markerData: IMarkerData, useMessage: boolean): string {
let result: string[] = [emptyString];
if (markerData.source) {
result.push(markerData.source.replace('¦', '\¦'));
result.push(markerData.source.replace('¦', '\\¦'));
} else {
result.push(emptyString);
}
if (markerData.code) {
if (typeof markerData.code === 'string') {
result.push(markerData.code.replace('¦', '\¦'));
result.push(markerData.code.replace('¦', '\\¦'));
} else {
result.push(markerData.code.value.replace('¦', '\¦'));
result.push(markerData.code.value.replace('¦', '\\¦'));
}
} else {
result.push(emptyString);
@@ -157,7 +157,7 @@ export namespace IMarkerData {
// Modifed to not include the message as part of the marker key to work around
// https://github.com/microsoft/vscode/issues/77475
if (markerData.message && useMessage) {
result.push(markerData.message.replace('¦', '\¦'));
result.push(markerData.message.replace('¦', '\\¦'));
} else {
result.push(emptyString);
}

View File

@@ -6,7 +6,7 @@
import BaseSeverity from 'vs/base/common/severity';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IAction } from 'vs/base/common/actions';
import { Event, Emitter } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
export import Severity = BaseSeverity;
@@ -318,16 +318,14 @@ export class NoOpNotification implements INotificationHandle {
readonly progress = new NoOpProgress();
private readonly _onDidClose: Emitter<void> = new Emitter();
readonly onDidClose: Event<void> = this._onDidClose.event;
readonly onDidClose = Event.None;
readonly onDidChangeVisibility = Event.None;
updateSeverity(severity: Severity): void { }
updateMessage(message: NotificationMessage): void { }
updateActions(actions?: INotificationActions): void { }
close(): void {
this._onDidClose.dispose();
}
close(): void { }
}
export class NoOpProgress implements INotificationProgress {

View File

@@ -17,7 +17,11 @@ export interface IProgressService {
_serviceBrand: undefined;
withProgress<R = any>(options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: (choice?: number) => void): Promise<R>;
withProgress<R = any>(
options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions,
task: (progress: IProgress<IProgressStep>) => Promise<R>,
onDidCancel?: (choice?: number) => void
): Promise<R>;
}
export interface IProgressIndicator {
@@ -45,19 +49,19 @@ export const enum ProgressLocation {
}
export interface IProgressOptions {
location: ProgressLocation | string;
title?: string;
source?: string;
total?: number;
cancellable?: boolean;
buttons?: string[];
readonly location: ProgressLocation | string;
readonly title?: string;
readonly source?: string;
readonly total?: number;
readonly cancellable?: boolean;
readonly buttons?: string[];
}
export interface IProgressNotificationOptions extends IProgressOptions {
readonly location: ProgressLocation.Notification;
readonly primaryActions?: ReadonlyArray<IAction>;
readonly secondaryActions?: ReadonlyArray<IAction>;
delay?: number;
readonly delay?: number;
}
export interface IProgressWindowOptions extends IProgressOptions {
@@ -66,8 +70,8 @@ export interface IProgressWindowOptions extends IProgressOptions {
}
export interface IProgressCompositeOptions extends IProgressOptions {
location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string;
delay?: number;
readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string;
readonly delay?: number;
}
export interface IProgressStep {
@@ -96,20 +100,14 @@ export interface IProgress<T> {
export class Progress<T> implements IProgress<T> {
private _callback: (data: T) => void;
private _value?: T;
get value(): T | undefined { return this._value; }
constructor(callback: (data: T) => void) {
this._callback = callback;
}
get value(): T | undefined {
return this._value;
}
constructor(private callback: (data: T) => void) { }
report(item: T) {
this._value = item;
this._callback(this._value);
this.callback(this._value);
}
}

View File

@@ -224,7 +224,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase
this.isWatching = true;
this._register(this.fileService.watch(this.file));
this._register(this.fileService.onFileChanges(e => {
this._register(this.fileService.onDidFilesChange(e => {
if (document.hasFocus()) {
return; // optimization: ignore changes from ourselves by checking for focus
}

View File

@@ -428,6 +428,10 @@ export const minimapError = registerColor('minimap.errorHighlight', { dark: new
export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.'));
export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color."));
export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hc: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color."));
export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hc: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering."));
export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hc: transparent(scrollbarSliderActiveBackground, 0.5) }, nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on."));
export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon."));
export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon."));
export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon."));

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
export const IUndoRedoService = createDecorator<IUndoRedoService>('undoRedoService');
export interface IUndoRedoContext {
replaceCurrentElement(others: IUndoRedoElement[]): void;
}
export interface IUndoRedoElement {
/**
* None, one or multiple resources that this undo/redo element impacts.
*/
readonly resources: URI[];
/**
* The label of the undo/redo element.
*/
readonly label: string;
/**
* Undo.
* Will always be called before `redo`.
* Can be called multiple times.
* e.g. `undo` -> `redo` -> `undo` -> `redo`
*/
undo(ctx: IUndoRedoContext): void;
/**
* Redo.
* Will always be called after `undo`.
* Can be called multiple times.
* e.g. `undo` -> `redo` -> `undo` -> `redo`
*/
redo(ctx: IUndoRedoContext): void;
/**
* Invalidate the edits concerning `resource`.
* i.e. the undo/redo stack for that particular resource has been destroyed.
*/
invalidate(resource: URI): boolean;
}
export interface IUndoRedoService {
_serviceBrand: undefined;
/**
* Add a new element to the `undo` stack.
* This will destroy the `redo` stack.
*/
pushElement(element: IUndoRedoElement): void;
/**
* Get the last pushed element. If the last pushed element has been undone, returns null.
*/
getLastElement(resource: URI): IUndoRedoElement | null;
/**
* Remove elements that target `resource`.
*/
removeElements(resource: URI): void;
canUndo(resource: URI): boolean;
undo(resource: URI): void;
redo(resource: URI): void;
canRedo(resource: URI): boolean;
}

View File

@@ -0,0 +1,241 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUndoRedoService, IUndoRedoElement } 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';
class StackElement {
public readonly actual: IUndoRedoElement;
public readonly label: string;
public readonly resources: URI[];
public readonly strResources: string[];
constructor(actual: IUndoRedoElement) {
this.actual = actual;
this.label = actual.label;
this.resources = actual.resources;
this.strResources = this.resources.map(resource => uriGetComparisonKey(resource));
}
public invalidate(resource: URI): void {
if (this.resources.length > 1) {
this.actual.invalidate(resource);
}
}
}
class ResourceEditStack {
public resource: URI;
public past: StackElement[];
public future: StackElement[];
constructor(resource: URI) {
this.resource = resource;
this.past = [];
this.future = [];
}
}
export class UndoRedoService implements IUndoRedoService {
_serviceBrand: undefined;
private readonly _editStacks: Map<string, ResourceEditStack>;
constructor() {
this._editStacks = new Map<string, ResourceEditStack>();
}
public pushElement(_element: IUndoRedoElement): void {
const element = new StackElement(_element);
for (let i = 0, len = element.resources.length; i < len; i++) {
const resource = element.resources[i];
const strResource = element.strResources[i];
let editStack: ResourceEditStack;
if (this._editStacks.has(strResource)) {
editStack = this._editStacks.get(strResource)!;
} else {
editStack = new ResourceEditStack(resource);
this._editStacks.set(strResource, editStack);
}
// remove the future
for (const futureElement of editStack.future) {
futureElement.invalidate(resource);
}
editStack.future = [];
editStack.past.push(element);
}
}
public getLastElement(resource: URI): IUndoRedoElement | null {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
if (editStack.future.length > 0) {
return null;
}
if (editStack.past.length === 0) {
return null;
}
return editStack.past[editStack.past.length - 1].actual;
}
return null;
}
public removeElements(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (const pastElement of editStack.past) {
pastElement.invalidate(resource);
}
for (const futureElement of editStack.future) {
futureElement.invalidate(resource);
}
this._editStacks.delete(strResource);
}
}
public canUndo(resource: URI): boolean {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
return (editStack.past.length > 0);
}
return false;
}
public undo(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
if (!this._editStacks.has(strResource)) {
return;
}
const editStack = this._editStacks.get(strResource)!;
if (editStack.past.length === 0) {
return;
}
const element = editStack.past[editStack.past.length - 1];
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null;
try {
element.actual.undo({
replaceCurrentElement: (others: IUndoRedoElement[]): void => {
replaceCurrentElement = others;
}
});
} catch (e) {
onUnexpectedError(e);
editStack.past.pop();
editStack.future.push(element);
return;
}
if (replaceCurrentElement === null) {
// regular case
editStack.past.pop();
editStack.future.push(element);
return;
}
const replaceCurrentElementMap = new Map<string, StackElement>();
for (const _replace of replaceCurrentElement) {
const replace = new StackElement(_replace);
for (const strResource of replace.strResources) {
replaceCurrentElementMap.set(strResource, replace);
}
}
for (let i = 0, len = element.strResources.length; i < len; i++) {
const strResource = element.strResources[i];
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (let j = editStack.past.length - 1; j >= 0; j--) {
if (editStack.past[j] === element) {
if (replaceCurrentElementMap.has(strResource)) {
editStack.past[j] = replaceCurrentElementMap.get(strResource)!;
} else {
editStack.past.splice(j, 1);
}
break;
}
}
}
}
}
public canRedo(resource: URI): boolean {
const strResource = uriGetComparisonKey(resource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
return (editStack.future.length > 0);
}
return false;
}
redo(resource: URI): void {
const strResource = uriGetComparisonKey(resource);
if (!this._editStacks.has(strResource)) {
return;
}
const editStack = this._editStacks.get(strResource)!;
if (editStack.future.length === 0) {
return;
}
const element = editStack.future[editStack.future.length - 1];
let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null;
try {
element.actual.redo({
replaceCurrentElement: (others: IUndoRedoElement[]): void => {
replaceCurrentElement = others;
}
});
} catch (e) {
onUnexpectedError(e);
editStack.future.pop();
editStack.past.push(element);
return;
}
if (replaceCurrentElement === null) {
// regular case
editStack.future.pop();
editStack.past.push(element);
return;
}
const replaceCurrentElementMap = new Map<string, StackElement>();
for (const _replace of replaceCurrentElement) {
const replace = new StackElement(_replace);
for (const strResource of replace.strResources) {
replaceCurrentElementMap.set(strResource, replace);
}
}
for (let i = 0, len = element.strResources.length; i < len; i++) {
const strResource = element.strResources[i];
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
for (let j = editStack.future.length - 1; j >= 0; j--) {
if (editStack.future[j] === element) {
if (replaceCurrentElementMap.has(strResource)) {
editStack.future[j] = replaceCurrentElementMap.get(strResource)!;
} else {
editStack.future.splice(j, 1);
}
break;
}
}
}
}
}
}

View File

@@ -174,7 +174,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
) {
super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
this._register(this.fileService.watch(dirname(file)));
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
}
async stop(): Promise<void> {

View File

@@ -39,7 +39,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
) {
super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService);
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
}
async pull(): Promise<void> {

View File

@@ -14,6 +14,9 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
private _onDidChangeToken: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());
readonly onDidChangeToken: Event<string | undefined> = this._onDidChangeToken.event;
private _onTokenFailed: Emitter<void> = this._register(new Emitter<void>());
readonly onTokenFailed: Event<void> = this._onTokenFailed.event;
private _token: string | undefined;
constructor() {
@@ -30,4 +33,8 @@ export class UserDataAuthTokenService extends Disposable implements IUserDataAut
this._onDidChangeToken.fire(token);
}
}
sendTokenFailed(): void {
this._onTokenFailed.fire();
}
}

View File

@@ -72,6 +72,13 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
return this.sync(loop, auto);
}
}
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.SessionExpired) {
this.logService.info('Auto Sync: Cloud has new session');
this.logService.info('Auto Sync: Resetting the local sync state.');
await this.userDataSyncService.resetLocal();
this.logService.info('Auto Sync: Completed resetting the local sync state.');
return this.sync(loop, auto);
}
this.logService.error(e);
this.successiveFailures++;
this._onError.fire(e instanceof UserDataSyncError ? { code: e.code, source: e.source } : { code: UserDataSyncErrorCode.Unknown });

View File

@@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
import { isEqual, joinPath } from 'vs/base/common/resources';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
@@ -120,23 +120,27 @@ export interface IUserData {
}
export interface IUserDataSyncStore {
url: string;
url: URI;
authenticationProviderId: string;
}
export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined {
const value = configurationService.getValue<IUserDataSyncStore>(CONFIGURATION_SYNC_STORE_KEY);
return value && value.url && value.authenticationProviderId ? value : undefined;
const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY);
if (value && value.url && value.authenticationProviderId) {
return {
url: joinPath(URI.parse(value.url), 'v1'),
authenticationProviderId: value.authenticationProviderId
};
}
return undefined;
}
export const ALL_RESOURCE_KEYS: ResourceKey[] = ['settings', 'keybindings', 'extensions', 'globalState'];
export type ResourceKey = 'settings' | 'keybindings' | 'extensions' | 'globalState';
export interface IUserDataManifest {
settings: string;
keybindings: string;
extensions: string;
globalState: string;
latest?: Record<ResourceKey, string>
session: string;
}
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
@@ -162,6 +166,7 @@ export enum UserDataSyncErrorCode {
TooLarge = 'TooLarge',
NoRef = 'NoRef',
TurnedOff = 'TurnedOff',
SessionExpired = 'SessionExpired',
// Local Errors
LocalPreconditionFailed = 'LocalPreconditionFailed',
@@ -303,9 +308,11 @@ export interface IUserDataAuthTokenService {
_serviceBrand: undefined;
readonly onDidChangeToken: Event<string | undefined>;
readonly onTokenFailed: Event<void>;
getToken(): Promise<string | undefined>;
setToken(accessToken: string | undefined): Promise<void>;
sendTokenFailed(): void;
}
export const IUserDataSyncLogService = createDecorator<IUserDataSyncLogService>('IUserDataSyncLogService');

View File

@@ -96,6 +96,7 @@ export class UserDataAuthTokenServiceChannel implements IServerChannel {
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeToken': return this.service.onDidChangeToken;
case 'onTokenFailed': return this.service.onTokenFailed;
}
throw new Error(`Event not found: ${event}`);
}

View File

@@ -14,11 +14,14 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { equals } from 'vs/base/common/arrays';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
type SyncErrorClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
const SESSION_ID_KEY = 'sync.sessionId';
export class UserDataSyncService extends Disposable implements IUserDataSyncService {
_serviceBrand: any;
@@ -48,6 +51,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser));
@@ -96,7 +100,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.setStatus(SyncStatus.Syncing);
}
const manifest = await this.userDataSyncStoreService.manifest();
let manifest = await this.userDataSyncStoreService.manifest();
// Server has no data but this machine was synced before
if (manifest === null && await this.hasPreviouslySynced()) {
@@ -104,14 +108,30 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
}
const sessionId = this.storageService.get(SESSION_ID_KEY, StorageScope.GLOBAL);
// Server session is different from client session
if (sessionId && manifest && sessionId !== manifest.session) {
throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
}
for (const synchroniser of this.synchronisers) {
try {
await synchroniser.sync(manifest ? manifest[synchroniser.resourceKey] : undefined);
await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined);
} catch (e) {
this.handleSyncError(e, synchroniser.source);
}
}
// After syncing, get the manifest if it was not available before
if (manifest === null) {
manifest = await this.userDataSyncStoreService.manifest();
}
// Update local session id
if (manifest && manifest.session !== sessionId) {
this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL);
}
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
} finally {
@@ -140,26 +160,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return synchroniser.accept(content);
}
private async hasPreviouslySynced(): Promise<boolean> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasPreviouslySynced()) {
return true;
}
}
return false;
}
private async hasLocalData(): Promise<boolean> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasLocalData()) {
return true;
}
}
return false;
}
async getRemoteContent(source: SyncSource, preview: boolean): Promise<string | null> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
@@ -189,6 +189,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
async resetLocal(): Promise<void> {
await this.checkEnablement();
this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL);
for (const synchroniser of this.synchronisers) {
try {
synchroniser.resetLocal();
@@ -199,6 +200,26 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
}
}
private async hasPreviouslySynced(): Promise<boolean> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasPreviouslySynced()) {
return true;
}
}
return false;
}
private async hasLocalData(): Promise<boolean> {
await this.checkEnablement();
for (const synchroniser of this.synchronisers) {
if (await synchroniser.hasLocalData()) {
return true;
}
}
return false;
}
private async resetRemote(): Promise<void> {
await this.checkEnablement();
try {

View File

@@ -6,7 +6,6 @@
import { Disposable, } from 'vs/base/common/lifecycle';
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
@@ -33,7 +32,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString();
const url = joinPath(this.userDataSyncStore.url, 'resource', key, 'latest').toString();
const headers: IHeaders = {};
// Disable caching as they are cached by synchronisers
headers['Cache-Control'] = 'no-cache';
@@ -65,7 +64,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key).toString();
const url = joinPath(this.userDataSyncStore.url, 'resource', key).toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
if (ref) {
headers['If-Match'] = ref;
@@ -89,7 +88,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', 'latest').toString();
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
const headers: IHeaders = { 'Content-Type': 'application/json' };
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
@@ -105,7 +104,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
throw new Error('No settings sync store url configured.');
}
const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString();
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
const headers: IHeaders = { 'Content-Type': 'text/plain' };
const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None);
@@ -134,6 +133,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
}
if (context.res.statusCode === 401) {
this.authTokenService.sendTokenFailed();
throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source);
}

View File

@@ -5,11 +5,7 @@
import * as assert from 'assert';
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
import { IStringDictionary } from 'vs/base/common/collections';
import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { URI } from 'vs/base/common/uri';
import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
import { TestUserDataSyncUtilService } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
suite('KeybindingsMerge - No Conflicts', () => {
@@ -613,7 +609,7 @@ suite('KeybindingsMerge - No Conflicts', () => {
});
async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) {
const userDataSyncUtilService = new MockUserDataSyncUtilService();
const userDataSyncUtilService = new TestUserDataSyncUtilService();
const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions();
return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService);
}
@@ -621,22 +617,3 @@ async function mergeKeybindings(localContent: string, remoteContent: string, bas
function stringify(value: any): string {
return JSON.stringify(value, null, '\t');
}
class MockUserDataSyncUtilService implements IUserDataSyncUtilService {
_serviceBrand: any;
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
const keys: IStringDictionary<string> = {};
for (const keybinding of userbindings) {
keys[keybinding] = keybinding;
}
return keys;
}
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
return { eol: '\n', insertSpaces: false, tabSize: 4 };
}
async ignoreExtensionsToSync(extensions: IExtensionIdentifier[]): Promise<void> { }
}

View File

@@ -0,0 +1,246 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRequestService } from 'vs/platform/request/common/request';
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataAuthTokenService, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { generateUuid } from 'vs/base/common/uuid';
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService';
import { IGlobalExtensionEnablementService, IExtensionManagementService, IExtensionGalleryService, DidInstallExtensionEvent, DidUninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import { Disposable } from 'vs/base/common/lifecycle';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { Emitter } from 'vs/base/common/event';
export class UserDataSyncClient extends Disposable {
readonly instantiationService: TestInstantiationService;
constructor(readonly testServer: UserDataSyncTestServer = new UserDataSyncTestServer()) {
super();
this.instantiationService = new TestInstantiationService();
}
async setUp(empty: boolean = false): Promise<void> {
const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory });
const userDataSyncHome = joinPath(userDataDirectory, '.sync');
const environmentService = this.instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{
userDataSyncHome,
settingsResource: joinPath(userDataDirectory, 'settings.json'),
settingsSyncPreviewResource: joinPath(userDataSyncHome, 'settings.json'),
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
keybindingsSyncPreviewResource: joinPath(userDataSyncHome, 'keybindings.json'),
argvResource: joinPath(userDataDirectory, 'argv.json'),
});
const logService = new NullLogService();
this.instantiationService.stub(ILogService, logService);
const fileService = this._register(new FileService(logService));
fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider());
this.instantiationService.stub(IFileService, fileService);
this.instantiationService.stub(IStorageService, new InMemoryStorageService());
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({
'configurationSync.store': {
url: this.testServer.url,
authenticationProviderId: 'test'
}
})));
const configurationService = new ConfigurationService(environmentService.settingsResource, fileService);
await configurationService.initialize();
this.instantiationService.stub(IConfigurationService, configurationService);
this.instantiationService.stub(IRequestService, this.testServer);
this.instantiationService.stub(IUserDataAuthTokenService, <Partial<IUserDataAuthTokenService>>{
onDidChangeToken: new Emitter<string | undefined>().event,
async getToken() { return 'token'; }
});
this.instantiationService.stub(IUserDataSyncLogService, logService);
this.instantiationService.stub(ITelemetryService, NullTelemetryService);
this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService));
this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService());
this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService));
this.instantiationService.stub(IGlobalExtensionEnablementService, this.instantiationService.createInstance(GlobalExtensionEnablementService));
this.instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
async getInstalled() { return []; },
onDidInstallExtension: new Emitter<DidInstallExtensionEvent>().event,
onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event,
});
this.instantiationService.stub(IExtensionGalleryService, <Partial<IExtensionGalleryService>>{
isEnabled() { return true; },
async getCompatibleExtension() { return null; }
});
this.instantiationService.stub(ISettingsSyncService, this.instantiationService.createInstance(SettingsSynchroniser));
this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService));
if (empty) {
await fileService.del(environmentService.settingsResource);
} else {
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
}
await configurationService.reloadConfiguration();
}
}
export class UserDataSyncTestServer implements IRequestService {
_serviceBrand: any;
readonly url: string = 'http://host:3000';
private session: string | null = null;
private readonly data: Map<ResourceKey, IUserData> = new Map<ResourceKey, IUserData>();
private _requests: { url: string, type: string, headers?: IHeaders }[] = [];
get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; }
private _responses: { status: number }[] = [];
get responses(): { status: number }[] { return this._responses; }
reset(): void { this._requests = []; this._responses = []; }
async resolveProxy(url: string): Promise<string | undefined> { return url; }
async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
const headers: IHeaders = {};
if (options.headers) {
if (options.headers['If-None-Match']) {
headers['If-None-Match'] = options.headers['If-None-Match'];
}
if (options.headers['If-Match']) {
headers['If-Match'] = options.headers['If-Match'];
}
}
this._requests.push({ url: options.url!, type: options.type!, headers });
const requestContext = await this.doRequest(options);
this._responses.push({ status: requestContext.res.statusCode! });
return requestContext;
}
private async doRequest(options: IRequestOptions): Promise<IRequestContext> {
const versionUrl = `${this.url}/v1/`;
const relativePath = options.url!.indexOf(versionUrl) === 0 ? options.url!.substring(versionUrl.length) : undefined;
const segments = relativePath ? relativePath.split('/') : [];
if (options.type === 'GET' && segments.length === 1 && segments[0] === 'manifest') {
return this.getManifest(options.headers);
}
if (options.type === 'GET' && segments.length === 3 && segments[0] === 'resource' && segments[2] === 'latest') {
return this.getLatestData(segments[1], options.headers);
}
if (options.type === 'POST' && segments.length === 2 && segments[0] === 'resource') {
return this.writeData(segments[1], options.data, options.headers);
}
if (options.type === 'DELETE' && segments.length === 1 && segments[0] === 'resource') {
return this.clear(options.headers);
}
return this.toResponse(501);
}
private async getManifest(headers?: IHeaders): Promise<IRequestContext> {
if (this.session) {
const latest: Record<ResourceKey, string> = Object.create({});
const manifest: IUserDataManifest = { session: this.session, latest };
this.data.forEach((value, key) => latest[key] = value.ref);
return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest));
}
return this.toResponse(204);
}
private async getLatestData(resource: string, headers: IHeaders = {}): Promise<IRequestContext> {
const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource);
if (resourceKey) {
const data = this.data.get(resourceKey);
if (!data) {
return this.toResponse(204, { etag: '0' });
}
if (headers['If-None-Match'] === data.ref) {
return this.toResponse(304);
}
return this.toResponse(200, { etag: data.ref }, data.content || '');
}
return this.toResponse(204);
}
private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise<IRequestContext> {
if (!headers['If-Match']) {
return this.toResponse(428);
}
if (!this.session) {
this.session = generateUuid();
}
const resourceKey = ALL_RESOURCE_KEYS.find(key => key === resource);
if (resourceKey) {
const data = this.data.get(resourceKey);
if (headers['If-Match'] !== (data ? data.ref : '0')) {
return this.toResponse(412);
}
const ref = `${parseInt(data?.ref || '0') + 1}`;
this.data.set(resourceKey, { ref, content });
return this.toResponse(200, { etag: ref });
}
return this.toResponse(204);
}
private async clear(headers?: IHeaders): Promise<IRequestContext> {
this.data.clear();
this.session = null;
return this.toResponse(204);
}
private toResponse(statusCode: number, headers?: IHeaders, data?: string): IRequestContext {
return {
res: {
headers: headers || {},
statusCode
},
stream: bufferToStream(VSBuffer.fromString(data || ''))
};
}
}
export class TestUserDataSyncUtilService implements IUserDataSyncUtilService {
_serviceBrand: any;
async resolveUserBindings(userbindings: string[]): Promise<IStringDictionary<string>> {
const keys: IStringDictionary<string> = {};
for (const keybinding of userbindings) {
keys[keybinding] = keybinding;
}
return keys;
}
async resolveFormattingOptions(file?: URI): Promise<FormattingOptions> {
return { eol: '\n', insertSpaces: false, tabSize: 4 };
}
}

View File

@@ -0,0 +1,555 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IUserDataSyncService, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, SyncSource } from 'vs/platform/userDataSync/common/userDataSync';
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { VSBuffer } from 'vs/base/common/buffer';
suite('UserDataSyncService', () => {
const disposableStore = new DisposableStore();
teardown(() => disposableStore.clear());
test('test first time sync ever', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
// Sync for first time
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ 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': '0' } },
// Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
]);
});
test('test first time sync ever with no data', async () => {
// Setup the client
const target = new UserDataSyncTestServer();
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp(true);
const testObject = client.instantiationService.get(IUserDataSyncService);
// Sync for first time
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ 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: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
]);
});
test('test first time sync from the client with no changes - pull', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
// Sync (pull) from the test client
target.reset();
await testObject.isFirstTimeSyncWithMerge();
await testObject.pull();
assert.deepEqual(target.requests, [
// Manifest
{ 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: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
]);
});
test('test first time sync from the client with changes - pull', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client with changes
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
const fileService = testClient.instantiationService.get(IFileService);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
// Sync (pull) from the test client
target.reset();
await testObject.isFirstTimeSyncWithMerge();
await testObject.pull();
assert.deepEqual(target.requests, [
// Manifest
{ 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: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
]);
});
test('test first time sync from the client with no changes - merge', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
// Sync (merge) from the test client
target.reset();
await testObject.isFirstTimeSyncWithMerge();
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ 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: {} },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
]);
});
test('test first time sync from the client with changes - merge', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client with changes
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const fileService = testClient.instantiationService.get(IFileService);
const environmentService = testClient.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
const testObject = testClient.instantiationService.get(IUserDataSyncService);
// Sync (merge) from the test client
target.reset();
await testObject.isFirstTimeSyncWithMerge();
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ 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: {} },
{ 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' } },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
]);
});
test('test sync when there are no changes', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// sync from the client again
target.reset();
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
]);
});
test('test sync when there are local changes', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
await testObject.sync();
target.reset();
// Do changes in the client
const fileService = client.instantiationService.get(IFileService);
const environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
// Sync from the client
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Settings
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
// Keybindings
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
// Global state
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } },
]);
});
test('test sync when there are remote changes', async () => {
const target = new UserDataSyncTestServer();
// Sync from first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Sync from test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// Do changes in first client and sync
const fileService = client.instantiationService.get(IFileService);
const environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
await client.instantiationService.get(IUserDataSyncService).sync();
// Sync from test client
target.reset();
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Settings
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } },
// Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } },
]);
});
test('test delete', async () => {
const target = new UserDataSyncTestServer();
// Sync from the client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// Reset from the client
target.reset();
await testObject.reset();
assert.deepEqual(target.requests, [
// Manifest
{ type: 'DELETE', url: `${target.url}/v1/resource`, headers: {} },
]);
});
test('test delete and sync', async () => {
const target = new UserDataSyncTestServer();
// Sync from the client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// Reset from the client
await testObject.reset();
// Sync again
target.reset();
await testObject.sync();
assert.deepEqual(target.requests, [
// Manifest
{ 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': '0' } },
// Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } },
// Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } },
// Extensions
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } },
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
]);
});
test('test delete on one client throws turned off error on other client while syncing', async () => {
const target = new UserDataSyncTestServer();
// Set up and sync from the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Set up and sync from the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// Reset from the first client
await client.instantiationService.get(IUserDataSyncService).reset();
// Sync from the test client
target.reset();
try {
await testObject.sync();
} catch (e) {
assert.ok(e instanceof UserDataSyncError);
assert.deepEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.TurnedOff);
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
]);
return;
}
throw assert.fail('Should fail with turned off error');
});
test('test creating new session from one client throws session expired error on another client while syncing', async () => {
const target = new UserDataSyncTestServer();
// Set up and sync from the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
await client.instantiationService.get(IUserDataSyncService).sync();
// Set up and sync from the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// Reset from the first client
await client.instantiationService.get(IUserDataSyncService).reset();
// Sync again from the first client to create new session
await client.instantiationService.get(IUserDataSyncService).sync();
// Sync from the test client
target.reset();
try {
await testObject.sync();
} catch (e) {
assert.ok(e instanceof UserDataSyncError);
assert.deepEqual((<UserDataSyncError>e).code, UserDataSyncErrorCode.SessionExpired);
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
]);
return;
}
throw assert.fail('Should fail with turned off error');
});
test('test sync status', async () => {
const target = new UserDataSyncTestServer();
// Setup the client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
const testObject = client.instantiationService.get(IUserDataSyncService);
// sync from the client
const actualStatuses: SyncStatus[] = [];
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
await testObject.sync();
disposable.dispose();
assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]);
});
test('test sync conflicts status', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
let fileService = client.instantiationService.get(IFileService);
let environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
fileService = testClient.instantiationService.get(IFileService);
environmentService = testClient.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
const testObject = testClient.instantiationService.get(IUserDataSyncService);
// sync from the client
await testObject.sync();
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assert.deepEqual(testObject.conflictsSources, [SyncSource.Settings]);
});
test('test sync will sync other non conflicted areas', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
let fileService = client.instantiationService.get(IFileService);
let environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client and get conflicts in settings
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
let testFileService = testClient.instantiationService.get(IFileService);
let testEnvironmentService = testClient.instantiationService.get(IEnvironmentService);
await testFileService.writeFile(testEnvironmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// sync from the first client with changes in keybindings
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await client.instantiationService.get(IUserDataSyncService).sync();
// sync from the test client
target.reset();
const actualStatuses: SyncStatus[] = [];
const disposable = testObject.onDidChangeStatus(status => actualStatuses.push(status));
await testObject.sync();
disposable.dispose();
assert.deepEqual(actualStatuses, []);
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
assert.deepEqual(target.requests, [
// Manifest
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
// Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } },
]);
});
test('test stop sync reset status', async () => {
const target = new UserDataSyncTestServer();
// Setup and sync from the first client
const client = disposableStore.add(new UserDataSyncClient(target));
await client.setUp();
let fileService = client.instantiationService.get(IFileService);
let environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
await client.instantiationService.get(IUserDataSyncService).sync();
// Setup the test client
const testClient = disposableStore.add(new UserDataSyncClient(target));
await testClient.setUp();
fileService = testClient.instantiationService.get(IFileService);
environmentService = testClient.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 16 })));
const testObject = testClient.instantiationService.get(IUserDataSyncService);
await testObject.sync();
// sync from the client
await testObject.stop();
assert.deepEqual(testObject.status, SyncStatus.Idle);
assert.deepEqual(testObject.conflictsSources, []);
});
});