Merge from vscode 2b0b9136329c181a9e381463a1f7dc3a2d105a34 (#4880)

This commit is contained in:
Karl Burtram
2019-04-05 10:09:18 -07:00
committed by GitHub
parent 9bd7e30d18
commit cb5bcf2248
433 changed files with 8915 additions and 8361 deletions

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI as Uri } from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IResolveContentOptions, IUpdateContentOptions, ITextSnapshot } from 'vs/platform/files/common/files';
import { ITextBufferFactory } from 'vs/editor/common/model';
@@ -30,7 +30,7 @@ export interface IBackupFileService {
* @param resource The resource that is backed up.
* @return The backup resource if any.
*/
loadBackupResource(resource: Uri): Promise<Uri | undefined>;
loadBackupResource(resource: URI): Promise<URI | undefined>;
/**
* Given a resource, returns the associated backup resource.
@@ -38,7 +38,7 @@ export interface IBackupFileService {
* @param resource The resource to get the backup resource for.
* @return The backup resource.
*/
toBackupResource(resource: Uri): Uri;
toBackupResource(resource: URI): URI;
/**
* Backs up a resource.
@@ -47,14 +47,14 @@ export interface IBackupFileService {
* @param content The content of the resource as snapshot.
* @param versionId The version id of the resource to backup.
*/
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void>;
backupResource(resource: URI, content: ITextSnapshot, versionId?: number): Promise<void>;
/**
* Gets a list of file backups for the current workspace.
*
* @return The list of backups.
*/
getWorkspaceFileBackups(): Promise<Uri[]>;
getWorkspaceFileBackups(): Promise<URI[]>;
/**
* Resolves the backup for the given resource.
@@ -62,14 +62,14 @@ export interface IBackupFileService {
* @param value The contents from a backup resource as stream.
* @return The backup file's backed up content as text buffer factory.
*/
resolveBackupContent(backup: Uri): Promise<ITextBufferFactory | undefined>;
resolveBackupContent(backup: URI): Promise<ITextBufferFactory | undefined>;
/**
* Discards the backup associated with a resource if it exists..
*
* @param resource The resource whose backup is being discarded discard to back up.
*/
discardResourceBackup(resource: Uri): Promise<void>;
discardResourceBackup(resource: URI): Promise<void>;
/**
* Discards all backups associated with the current workspace and prevents further backups from

View File

@@ -242,7 +242,7 @@ class BackupFileServiceImpl implements IBackupFileService {
const backupResource = this.toBackupResource(resource);
return this.ioOperationQueues.queueFor(backupResource).queue(() => {
return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource));
return pfs.rimraf(backupResource.fsPath, pfs.RimRafMode.MOVE).then(() => model.remove(backupResource));
});
});
}
@@ -251,7 +251,7 @@ class BackupFileServiceImpl implements IBackupFileService {
this.isShuttingDown = true;
return this.ready.then(model => {
return pfs.del(this.backupWorkspacePath).then(() => model.clear());
return pfs.rimraf(this.backupWorkspacePath, pfs.RimRafMode.MOVE).then(() => model.clear());
});
}

View File

@@ -12,17 +12,18 @@ import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { URI as Uri } from 'vs/base/common/uri';
import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { TestContextService, TestTextResourceConfigurationService, TestLifecycleService, TestEnvironmentService, TestStorageService, TestWindowService } from 'vs/workbench/test/workbenchTestServices';
import { TestContextService, TestTextResourceConfigurationService, TestEnvironmentService, TestWindowService } from 'vs/workbench/test/workbenchTestServices';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { DefaultEndOfLine } from 'vs/editor/common/model';
import { snapshotToString } from 'vs/platform/files/common/files';
import { Schemas } from 'vs/base/common/network';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { NullLogService } from 'vs/platform/log/common/log';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
const backupHome = path.join(parentDir, 'Backups');
@@ -55,7 +56,14 @@ class TestBackupWindowService extends TestWindowService {
class TestBackupFileService extends BackupFileService {
constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true });
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
fileService.setLegacyService(new LegacyFileService(
fileService,
new TestContextService(new Workspace(workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))),
TestEnvironmentService,
new TestTextResourceConfigurationService(),
));
const windowService = new TestBackupWindowService(workspaceBackupPath);
super(windowService, fileService);
@@ -73,7 +81,7 @@ suite('BackupFileService', () => {
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
// Delete any existing backups completely and then re-create it.
return pfs.del(backupHome, os.tmpdir()).then(() => {
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => {
return pfs.mkdirp(backupHome).then(() => {
return pfs.writeFile(workspacesJsonPath, '');
});
@@ -81,7 +89,7 @@ suite('BackupFileService', () => {
});
teardown(() => {
return pfs.del(backupHome, os.tmpdir());
return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE);
});
suite('hashPath', () => {

View File

@@ -11,7 +11,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IBroadcastService, IBroadcast } from 'vs/workbench/services/broadcast/common/broadcast';
export class BroadcastService extends Disposable implements IBroadcastService {
class BroadcastService extends Disposable implements IBroadcastService {
_serviceBrand: any;
private readonly _onBroadcast: Emitter<IBroadcast> = this._register(new Emitter<IBroadcast>());

View File

@@ -327,7 +327,7 @@ export class BulkEdit {
// delete file
if (await this._fileService.exists(edit.oldUri)) {
let useTrash = this._configurationService.getValue<boolean>('files.enableTrash');
if (useTrash && !(await this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) {
if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) {
useTrash = false; // not supported by provider
}
await this._textFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive });

View File

@@ -10,12 +10,14 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { timeout } from 'vs/base/common/async';
export class CommandService extends Disposable implements ICommandService {
_serviceBrand: any;
private _extensionHostIsReady: boolean = false;
private _starActivation: Promise<void> | null;
private readonly _onWillExecuteCommand: Emitter<ICommandEvent> = this._register(new Emitter<ICommandEvent>());
public readonly onWillExecuteCommand: Event<ICommandEvent> = this._onWillExecuteCommand.event;
@@ -27,6 +29,18 @@ export class CommandService extends Disposable implements ICommandService {
) {
super();
this._extensionService.whenInstalledExtensionsRegistered().then(value => this._extensionHostIsReady = value);
this._starActivation = null;
}
private _activateStar(): Promise<void> {
if (!this._starActivation) {
// wait for * activation, limited to at most 30s
this._starActivation = Promise.race<any>([
this._extensionService.activateByEvent(`*`),
timeout(30000)
]);
}
return this._starActivation;
}
executeCommand<T>(id: string, ...args: any[]): Promise<T> {
@@ -44,10 +58,13 @@ export class CommandService extends Disposable implements ICommandService {
} else {
let waitFor = activation;
if (!commandIsRegistered) {
waitFor = Promise.race<any>([
// race activation events against command registration
Promise.all([activation, this._extensionService.activateByEvent(`*`)]),
Event.toPromise(Event.filter(CommandsRegistry.onDidRegisterCommand, e => e === id)),
waitFor = Promise.all([
activation,
Promise.race<any>([
// race * activation against command registration
this._activateStar(),
Event.toPromise(Event.filter(CommandsRegistry.onDidRegisterCommand, e => e === id))
]),
]);
}
return (waitFor as Promise<any>).then(_ => this._tryExecuteCommand(id, args));

View File

@@ -134,4 +134,43 @@ suite('CommandService', function () {
dispose(dispoables);
});
});
test('issue #71471: wait for onCommand activation even if a command is registered', () => {
let expectedOrder: string[] = ['registering command', 'resolving activation event', 'executing command'];
let actualOrder: string[] = [];
let disposables: IDisposable[] = [];
let service = new CommandService(new InstantiationService(), new class extends NullExtensionService {
activateByEvent(event: string): Promise<void> {
if (event === '*') {
return new Promise(() => { }); //forever promise...
}
if (event.indexOf('onCommand:') === 0) {
return new Promise(resolve => {
setTimeout(() => {
// Register the command after some time
actualOrder.push('registering command');
let reg = CommandsRegistry.registerCommand(event.substr('onCommand:'.length), () => {
actualOrder.push('executing command');
});
disposables.push(reg);
setTimeout(() => {
// Resolve the activation event after some more time
actualOrder.push('resolving activation event');
resolve();
}, 10);
}, 10);
});
}
return Promise.resolve();
}
}, new NullLogService());
return service.executeCommand('farboo2').then(() => {
assert.deepEqual(actualOrder, expectedOrder);
dispose(disposables);
});
});
});

View File

@@ -4,43 +4,41 @@
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createHash } from 'crypto';
import * as resources from 'vs/base/common/resources';
import { Event, Emitter } from 'vs/base/common/event';
import * as pfs from 'vs/base/node/pfs';
import * as errors from 'vs/base/common/errors';
import * as collections from 'vs/base/common/collections';
import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler, Delayer } from 'vs/base/common/async';
import { FileChangeType, FileChangesEvent, IContent, IFileService } from 'vs/platform/files/common/files';
import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
import { RunOnceScheduler } from 'vs/base/common/async';
import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels';
import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels';
import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration';
import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import * as extfs from 'vs/base/node/extfs';
import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration';
import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService';
import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { extname, join } from 'vs/base/common/path';
import { equals } from 'vs/base/common/objects';
import { Schemas } from 'vs/base/common/network';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationModel, compare } from 'vs/platform/configuration/common/configuration';
import { FileServiceBasedUserConfiguration, NodeBasedUserConfiguration } from 'vs/platform/configuration/node/configuration';
import { createSHA1 } from 'vs/base/browser/hash';
export class LocalUserConfiguration extends Disposable {
private readonly userConfigurationResource: URI;
private userConfiguration: NodeBasedUserConfiguration | FileServiceBasedUserConfiguration;
private changeDisposable: IDisposable = Disposable.None;
private readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
public readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
constructor(
environmentService: IEnvironmentService
userConfigurationResource: URI,
configurationFileService: IConfigurationFileService
) {
super();
this.userConfiguration = this._register(new NodeBasedUserConfiguration(environmentService.appSettingsPath));
this._register(this.userConfiguration.onDidChangeConfiguration(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)));
this.userConfigurationResource = userConfigurationResource;
this.userConfiguration = this._register(new NodeBasedUserConfiguration(this.userConfigurationResource, configurationFileService));
}
initialize(): Promise<ConfigurationModel> {
@@ -52,6 +50,21 @@ export class LocalUserConfiguration extends Disposable {
}
async adopt(fileService: IFileService): Promise<ConfigurationModel | null> {
if (this.userConfiguration instanceof NodeBasedUserConfiguration) {
const oldConfigurationModel = this.userConfiguration.getConfigurationModel();
this.userConfiguration.dispose();
dispose(this.changeDisposable);
let newConfigurationModel = new ConfigurationModel();
this.userConfiguration = this._register(new FileServiceBasedUserConfiguration(this.userConfigurationResource, fileService));
this.changeDisposable = this._register(this.userConfiguration.onDidChangeConfiguration(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)));
newConfigurationModel = await this.userConfiguration.initialize();
const { added, updated, removed } = compare(oldConfigurationModel, newConfigurationModel);
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
return newConfigurationModel;
}
}
return null;
}
}
@@ -66,10 +79,10 @@ export class RemoteUserConfiguration extends Disposable {
constructor(
remoteAuthority: string,
environmentService: IEnvironmentService
configurationCache: IConfigurationCache
) {
super();
this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, environmentService);
this._userConfiguration = this._cachedConfiguration = new CachedUserConfiguration(remoteAuthority, configurationCache);
}
initialize(): Promise<ConfigurationModel> {
@@ -108,22 +121,157 @@ export class RemoteUserConfiguration extends Disposable {
}
}
class NodeBasedUserConfiguration extends Disposable {
private configuraitonModel: ConfigurationModel = new ConfigurationModel();
constructor(
private readonly userConfigurationResource: URI,
private readonly configurationFileService: IConfigurationFileService
) {
super();
}
initialize(): Promise<ConfigurationModel> {
return this._load();
}
reload(): Promise<ConfigurationModel> {
return this._load();
}
getConfigurationModel(): ConfigurationModel {
return this.configuraitonModel;
}
async _load(): Promise<ConfigurationModel> {
const exists = await this.configurationFileService.exists(this.userConfigurationResource);
if (exists) {
try {
const content = await this.configurationFileService.resolveContent(this.userConfigurationResource);
const parser = new ConfigurationModelParser(this.userConfigurationResource.toString());
parser.parse(content);
this.configuraitonModel = parser.configurationModel;
} catch (e) {
// ignore error
errors.onUnexpectedError(e);
this.configuraitonModel = new ConfigurationModel();
}
} else {
this.configuraitonModel = new ConfigurationModel();
}
return this.configuraitonModel;
}
}
export class FileServiceBasedUserConfiguration extends Disposable {
private readonly reloadConfigurationScheduler: RunOnceScheduler;
protected readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
private fileWatcherDisposable: IDisposable = Disposable.None;
private directoryWatcherDisposable: IDisposable = Disposable.None;
constructor(
private readonly configurationResource: URI,
private readonly fileService: IFileService
) {
super();
this._register(fileService.onFileChanges(e => this.handleFileEvents(e)));
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
this._register(toDisposable(() => {
this.stopWatchingResource();
this.stopWatchingDirectory();
}));
}
private watchResource(): void {
this.fileWatcherDisposable = this.fileService.watch(this.configurationResource);
}
private stopWatchingResource(): void {
this.fileWatcherDisposable.dispose();
this.fileWatcherDisposable = Disposable.None;
}
private watchDirectory(): void {
const directory = resources.dirname(this.configurationResource);
this.directoryWatcherDisposable = this.fileService.watch(directory);
}
private stopWatchingDirectory(): void {
this.directoryWatcherDisposable.dispose();
this.directoryWatcherDisposable = Disposable.None;
}
async initialize(): Promise<ConfigurationModel> {
const exists = await this.fileService.exists(this.configurationResource);
this.onResourceExists(exists);
return this.reload();
}
async reload(): Promise<ConfigurationModel> {
try {
const content = await this.fileService.resolveContent(this.configurationResource);
const parser = new ConfigurationModelParser(this.configurationResource.toString());
parser.parse(content.value);
return parser.configurationModel;
} catch (e) {
return new ConfigurationModel();
}
}
private async handleFileEvents(event: FileChangesEvent): Promise<void> {
const events = event.changes;
let affectedByChanges = false;
// Find changes that affect the resource
for (const event of events) {
affectedByChanges = resources.isEqual(this.configurationResource, event.resource);
if (affectedByChanges) {
if (event.type === FileChangeType.ADDED) {
this.onResourceExists(true);
} else if (event.type === FileChangeType.DELETED) {
this.onResourceExists(false);
}
break;
}
}
if (affectedByChanges) {
this.reloadConfigurationScheduler.schedule();
}
}
private onResourceExists(exists: boolean): void {
if (exists) {
this.stopWatchingDirectory();
this.watchResource();
} else {
this.stopWatchingResource();
this.watchDirectory();
}
}
}
class CachedUserConfiguration extends Disposable {
private readonly _onDidChange: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
readonly onDidChange: Event<ConfigurationModel> = this._onDidChange.event;
private readonly cachedFolderPath: string;
private readonly cachedConfigurationPath: string;
private readonly key: ConfigurationKey;
private configurationModel: ConfigurationModel;
constructor(
remoteAuthority: string,
private environmentService: IEnvironmentService
private readonly configurationCache: IConfigurationCache
) {
super();
this.cachedFolderPath = join(this.environmentService.userDataPath, 'CachedConfigurations', 'user', remoteAuthority);
this.cachedConfigurationPath = join(this.cachedFolderPath, 'configuration.json');
this.key = { type: 'user', key: remoteAuthority };
this.configurationModel = new ConfigurationModel();
}
@@ -135,44 +283,29 @@ class CachedUserConfiguration extends Disposable {
return this.reload();
}
reload(): Promise<ConfigurationModel> {
return pfs.readFile(this.cachedConfigurationPath)
.then(content => content.toString(), () => '')
.then(content => {
try {
const parsed: IConfigurationModel = JSON.parse(content);
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
} catch (e) {
}
return this.configurationModel;
});
async reload(): Promise<ConfigurationModel> {
const content = await this.configurationCache.read(this.key);
try {
const parsed: IConfigurationModel = JSON.parse(content);
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
} catch (e) {
}
return this.configurationModel;
}
updateConfiguration(configurationModel: ConfigurationModel): Promise<void> {
const raw = JSON.stringify(configurationModel.toJSON());
return this.createCachedFolder().then(created => {
if (created) {
return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath);
}
return undefined;
});
if (configurationModel.keys.length) {
return this.configurationCache.write(this.key, JSON.stringify(configurationModel.toJSON()));
} else {
return this.configurationCache.remove(this.key);
}
}
private createCachedFolder(): Promise<boolean> {
return Promise.resolve(pfs.exists(this.cachedFolderPath))
.then(undefined, () => false)
.then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false));
}
}
export interface IWorkspaceIdentifier {
id: string;
configPath: URI;
}
export class WorkspaceConfiguration extends Disposable {
private readonly _cachedConfiguration: CachedWorkspaceConfiguration;
private readonly _configurationFileService: IConfigurationFileService;
private _workspaceConfiguration: IWorkspaceConfiguration;
private _workspaceIdentifier: IWorkspaceIdentifier | null = null;
private _fileService: IFileService | null = null;
@@ -181,10 +314,12 @@ export class WorkspaceConfiguration extends Disposable {
public readonly onDidUpdateConfiguration: Event<void> = this._onDidUpdateConfiguration.event;
constructor(
environmentService: IEnvironmentService
configurationCache: IConfigurationCache,
configurationFileService: IConfigurationFileService
) {
super();
this._cachedConfiguration = new CachedWorkspaceConfiguration(environmentService);
this._cachedConfiguration = new CachedWorkspaceConfiguration(configurationCache);
this._configurationFileService = configurationFileService;
this._workspaceConfiguration = this._cachedConfiguration;
}
@@ -251,7 +386,7 @@ export class WorkspaceConfiguration extends Disposable {
if (this._workspaceIdentifier.configPath.scheme === Schemas.file) {
if (!(this._workspaceConfiguration instanceof NodeBasedWorkspaceConfiguration)) {
dispose(this._workspaceConfiguration);
this._workspaceConfiguration = new NodeBasedWorkspaceConfiguration();
this._workspaceConfiguration = new NodeBasedWorkspaceConfiguration(this._configurationFileService);
return true;
}
return false;
@@ -306,14 +441,17 @@ abstract class AbstractWorkspaceConfiguration extends Disposable implements IWor
return this._workspaceIdentifier;
}
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
async load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
this._workspaceIdentifier = workspaceIdentifier;
return this.loadWorkspaceConfigurationContents(workspaceIdentifier)
.then(contents => {
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(workspaceIdentifier.id);
this.workspaceConfigurationModelParser.parse(contents);
this.consolidate();
});
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(workspaceIdentifier.id);
let contents = '';
try {
contents = (await this.loadWorkspaceConfigurationContents(workspaceIdentifier.configPath)) || '';
} catch (e) {
errors.onUnexpectedError(e);
}
this.workspaceConfigurationModelParser.parse(contents);
this.consolidate();
}
getConfigurationModel(): ConfigurationModel {
@@ -338,17 +476,21 @@ abstract class AbstractWorkspaceConfiguration extends Disposable implements IWor
this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel);
}
protected abstract loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise<string>;
protected abstract loadWorkspaceConfigurationContents(workspaceConfigurationResource: URI): Promise<string | undefined>;
}
class NodeBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration {
protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise<string> {
return pfs.readFile(workspaceIdentifier.configPath.fsPath)
.then(contents => contents.toString(), e => {
errors.onUnexpectedError(e);
return '';
});
constructor(private readonly configurationFileService: IConfigurationFileService) {
super();
}
protected async loadWorkspaceConfigurationContents(workspaceConfigurationResource: URI): Promise<string | undefined> {
const exists = await this.configurationFileService.exists(workspaceConfigurationResource);
if (exists) {
return this.configurationFileService.resolveContent(workspaceConfigurationResource);
}
return undefined;
}
}
@@ -356,6 +498,8 @@ class NodeBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration {
class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration {
private workspaceConfig: URI | null = null;
private workspaceConfigWatcher: IDisposable;
private readonly reloadConfigurationScheduler: RunOnceScheduler;
constructor(private fileService: IFileService, from?: IWorkspaceConfiguration) {
@@ -363,33 +507,24 @@ class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfigurat
this.workspaceConfig = from && from.workspaceIdentifier ? from.workspaceIdentifier.configPath : null;
this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
this.watchWorkspaceConfigurationFile();
this._register(toDisposable(() => this.unWatchWorkspaceConfigurtionFile()));
this.workspaceConfigWatcher = this.watchWorkspaceConfigurationFile();
}
private watchWorkspaceConfigurationFile(): void {
private watchWorkspaceConfigurationFile(): IDisposable {
if (this.workspaceConfig) {
this.fileService.watch(this.workspaceConfig);
return this.fileService.watch(this.workspaceConfig);
}
return Disposable.None;
}
private unWatchWorkspaceConfigurtionFile(): void {
if (this.workspaceConfig) {
this.fileService.unwatch(this.workspaceConfig);
protected loadWorkspaceConfigurationContents(workspaceConfigurationResource: URI): Promise<string> {
if (!(this.workspaceConfig && resources.isEqual(this.workspaceConfig, workspaceConfigurationResource))) {
dispose(this.workspaceConfigWatcher);
this.workspaceConfig = workspaceConfigurationResource;
this.workspaceConfigWatcher = this.watchWorkspaceConfigurationFile();
}
}
protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise<string> {
if (!(this.workspaceConfig && resources.isEqual(this.workspaceConfig, workspaceIdentifier.configPath))) {
this.unWatchWorkspaceConfigurtionFile();
this.workspaceConfig = workspaceIdentifier.configPath;
this.watchWorkspaceConfigurationFile();
}
return this.fileService.resolveContent(this.workspaceConfig)
.then(content => content.value, e => {
errors.onUnexpectedError(e);
return '';
});
return this.fileService.resolveContent(this.workspaceConfig).then(content => content.value);
}
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
@@ -407,6 +542,12 @@ class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfigurat
}
}
}
dispose(): void {
super.dispose();
dispose(this.workspaceConfigWatcher);
}
}
class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration {
@@ -414,25 +555,24 @@ class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfi
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
private cachedWorkspacePath: string;
private cachedConfigurationPath: string;
workspaceConfigurationModelParser: WorkspaceConfigurationModelParser;
workspaceSettings: ConfigurationModel;
constructor(private environmentService: IEnvironmentService) {
constructor(private readonly configurationCache: IConfigurationCache) {
super();
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser('');
this.workspaceSettings = new ConfigurationModel();
}
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
this.createPaths(workspaceIdentifier);
return pfs.readFile(this.cachedConfigurationPath)
.then(contents => {
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this.cachedConfigurationPath);
this.workspaceConfigurationModelParser.parse(contents.toString());
this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel);
}, () => { });
async load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
try {
const key = this.getKey(workspaceIdentifier);
const contents = await this.configurationCache.read(key);
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(key.key);
this.workspaceConfigurationModelParser.parse(contents);
this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel);
} catch (e) {
}
}
get workspaceIdentifier(): IWorkspaceIdentifier | null {
@@ -457,38 +597,24 @@ class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfi
async updateWorkspace(workspaceIdentifier: IWorkspaceIdentifier, configurationModel: ConfigurationModel): Promise<void> {
try {
this.createPaths(workspaceIdentifier);
const key = this.getKey(workspaceIdentifier);
if (configurationModel.keys.length) {
const exists = await pfs.exists(this.cachedWorkspacePath);
if (!exists) {
await pfs.mkdirp(this.cachedWorkspacePath);
}
const raw = JSON.stringify(configurationModel.toJSON().contents);
await pfs.writeFile(this.cachedConfigurationPath, raw);
await this.configurationCache.write(key, JSON.stringify(configurationModel.toJSON().contents));
} else {
pfs.rimraf(this.cachedWorkspacePath);
await this.configurationCache.remove(key);
}
} catch (error) {
errors.onUnexpectedError(error);
}
}
private createPaths(workspaceIdentifier: IWorkspaceIdentifier) {
this.cachedWorkspacePath = join(this.environmentService.userDataPath, 'CachedConfigurations', 'workspaces', workspaceIdentifier.id);
this.cachedConfigurationPath = join(this.cachedWorkspacePath, 'workspace.json');
private getKey(workspaceIdentifier: IWorkspaceIdentifier): ConfigurationKey {
return {
type: 'workspaces',
key: workspaceIdentifier.id
};
}
}
function isFolderConfigurationFile(resource: URI): boolean {
const configurationNameResource = URI.from({ scheme: resource.scheme, path: resources.basename(resource) });
return [`${FOLDER_SETTINGS_NAME}.json`, `${TASKS_CONFIGURATION_KEY}.json`, `${LAUNCH_CONFIGURATION_KEY}.json`].some(configurationFileName =>
resources.isEqual(configurationNameResource, URI.from({ scheme: resource.scheme, path: configurationFileName }))); // only workspace config files
}
function isFolderSettingsConfigurationFile(resource: URI): boolean {
return resources.isEqual(URI.from({ scheme: resource.scheme, path: resources.basename(resource) }), URI.from({ scheme: resource.scheme, path: `${FOLDER_SETTINGS_NAME}.json` }));
}
export interface IFolderConfiguration extends IDisposable {
readonly onDidChange: Event<void>;
readonly loaded: boolean;
@@ -503,12 +629,16 @@ export abstract class AbstractFolderConfiguration extends Disposable implements
private _cache: ConfigurationModel;
private _loaded: boolean = false;
private readonly configurationNames: string[];
protected readonly configurationResources: URI[];
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(protected readonly folder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) {
constructor(protected readonly configurationFolder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) {
super();
this.configurationNames = [FOLDER_SETTINGS_NAME /*First one should be settings */, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY];
this.configurationResources = this.configurationNames.map(name => resources.joinPath(this.configurationFolder, `${name}.json`));
this._folderSettingsModelParser = from ? from._folderSettingsModelParser : new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]);
this._standAloneConfigurations = from ? from._standAloneConfigurations : [];
this._cache = from ? from._cache : new ConfigurationModel();
@@ -518,23 +648,37 @@ export abstract class AbstractFolderConfiguration extends Disposable implements
return this._loaded;
}
loadConfiguration(): Promise<ConfigurationModel> {
return this.loadFolderConfigurationContents()
.then((contents) => {
async loadConfiguration(): Promise<ConfigurationModel> {
const configurationContents = await Promise.all(this.configurationResources.map(resource =>
this.loadConfigurationResourceContents(resource)
.then(undefined, error => {
/* never fail */
errors.onUnexpectedError(error);
return undefined;
})));
// reset
this._standAloneConfigurations = [];
this._folderSettingsModelParser.parse('');
// reset
this._standAloneConfigurations = [];
this._folderSettingsModelParser.parse('');
// parse
this.parseContents(contents);
// parse
if (configurationContents[0]) {
this._folderSettingsModelParser.parse(configurationContents[0]);
}
for (let index = 1; index < configurationContents.length; index++) {
const contents = configurationContents[index];
if (contents) {
const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.configurationResources[index].toString(), this.configurationNames[index]);
standAloneConfigurationModelParser.parse(contents);
this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel);
}
}
// Consolidate (support *.json files in the workspace settings folder)
this.consolidate();
// Consolidate (support *.json files in the workspace settings folder)
this.consolidate();
this._loaded = true;
return this._cache;
});
this._loaded = true;
return this._cache;
}
reprocess(): ConfigurationModel {
@@ -550,109 +694,41 @@ export abstract class AbstractFolderConfiguration extends Disposable implements
this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations);
}
private parseContents(contents: { resource: URI, value: string }[]): void {
for (const content of contents) {
if (isFolderSettingsConfigurationFile(content.resource)) {
this._folderSettingsModelParser.parse(content.value);
} else {
const name = resources.basename(content.resource);
const matches = /([^\.]*)*\.json/.exec(name);
if (matches && matches[1]) {
const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(content.resource.toString(), matches[1]);
standAloneConfigurationModelParser.parse(content.value);
this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel);
}
}
}
}
protected abstract loadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]>;
protected abstract loadConfigurationResourceContents(configurationResource: URI): Promise<string | undefined>;
}
export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration {
private readonly folderConfigurationPath: URI;
constructor(folder: URI, configFolderRelativePath: string, workbenchState: WorkbenchState) {
super(folder, workbenchState);
this.folderConfigurationPath = resources.joinPath(folder, configFolderRelativePath);
constructor(private readonly configurationFileService: IConfigurationFileService, configurationFolder: URI, workbenchState: WorkbenchState) {
super(configurationFolder, workbenchState);
}
protected loadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]> {
return this.resolveStat(this.folderConfigurationPath).then(stat => {
if (!stat.isDirectory || !stat.children) {
return Promise.resolve([]);
}
return this.resolveContents(stat.children.filter(stat => isFolderConfigurationFile(stat.resource))
.map(stat => stat.resource));
}, err => [] /* never fail this call */)
.then(undefined, e => {
errors.onUnexpectedError(e);
return [];
});
}
private resolveContents(resources: URI[]): Promise<{ resource: URI, value: string }[]> {
return Promise.all(resources.map(resource =>
pfs.readFile(resource.fsPath)
.then(contents => ({ resource, value: contents.toString() }))));
}
private resolveStat(resource: URI): Promise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }> {
return new Promise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }>((c, e) => {
extfs.readdir(resource.fsPath, (error, children) => {
if (error) {
if ((<any>error).code === 'ENOTDIR') {
c({ resource });
} else {
e(error);
}
} else {
c({
resource,
isDirectory: true,
children: children.map(child => { return { resource: resources.joinPath(resource, child) }; })
});
}
});
});
protected async loadConfigurationResourceContents(configurationResource: URI): Promise<string | undefined> {
const exists = await this.configurationFileService.exists(configurationResource);
if (exists) {
return this.configurationFileService.resolveContent(configurationResource);
}
return undefined;
}
}
export class FileServiceBasedFolderConfiguration extends AbstractFolderConfiguration {
private reloadConfigurationScheduler: RunOnceScheduler;
private readonly folderConfigurationPath: URI;
private readonly loadConfigurationDelayer = new Delayer<Array<{ resource: URI, value: string }>>(50);
private changeEventTriggerScheduler: RunOnceScheduler;
constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) {
super(folder, workbenchState, from);
this.folderConfigurationPath = resources.joinPath(folder, configFolderRelativePath);
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
constructor(configurationFolder: URI, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) {
super(configurationFolder, workbenchState, from);
this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
}
protected loadFolderConfigurationContents(): Promise<Array<{ resource: URI, value: string }>> {
return Promise.resolve(this.loadConfigurationDelayer.trigger(() => this.doLoadFolderConfigurationContents()));
}
private doLoadFolderConfigurationContents(): Promise<Array<{ resource: URI, value: string }>> {
const workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: Promise<IContent | undefined> } = Object.create(null);
const bulkContentFetchromise = Promise.resolve(this.fileService.resolve(this.folderConfigurationPath))
.then(stat => {
if (stat.isDirectory && stat.children) {
stat.children
.filter(child => isFolderConfigurationFile(child.resource))
.forEach(child => {
const folderRelativePath = this.toFolderRelativePath(child.resource);
if (folderRelativePath) {
workspaceFilePathToConfiguration[folderRelativePath] = Promise.resolve(this.fileService.resolveContent(child.resource)).then(undefined, errors.onUnexpectedError);
}
});
}
}).then(undefined, err => [] /* never fail this call */);
return bulkContentFetchromise.then(() => Promise.all<IContent>(collections.values(workspaceFilePathToConfiguration))).then(contents => contents.filter(content => content !== undefined));
protected async loadConfigurationResourceContents(configurationResource: URI): Promise<string | undefined> {
const exists = await this.fileService.exists(configurationResource);
if (exists) {
const contents = await this.fileService.resolveContent(configurationResource);
return contents.value;
}
return undefined;
}
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
@@ -664,9 +740,9 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
const resource = events[i].resource;
const basename = resources.basename(resource);
const isJson = extname(basename) === '.json';
const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && basename === this.configFolderRelativePath);
const isConfigurationFolderDeleted = (events[i].type === FileChangeType.DELETED && resources.isEqual(resource, this.configurationFolder));
if (!isJson && !isDeletedSettingsFolder) {
if (!isJson && !isConfigurationFolderDeleted) {
continue; // only JSON files or the actual settings folder
}
@@ -676,72 +752,69 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
}
// Handle case where ".vscode" got deleted
if (isDeletedSettingsFolder) {
if (isConfigurationFolderDeleted) {
affectedByChanges = true;
break;
}
// only valid workspace config files
if (!isFolderConfigurationFile(resource)) {
continue;
if (this.configurationResources.some(configurationResource => resources.isEqual(configurationResource, resource))) {
affectedByChanges = true;
break;
}
affectedByChanges = true;
break;
}
if (affectedByChanges) {
this.reloadConfigurationScheduler.schedule();
this.changeEventTriggerScheduler.schedule();
}
}
private toFolderRelativePath(resource: URI): string | undefined {
if (resources.isEqualOrParent(resource, this.folderConfigurationPath)) {
return resources.relativePath(this.folderConfigurationPath, resource);
if (resources.isEqualOrParent(resource, this.configurationFolder)) {
return resources.relativePath(this.configurationFolder, resource);
}
return undefined;
}
}
export class CachedFolderConfiguration extends Disposable implements IFolderConfiguration {
class CachedFolderConfiguration extends Disposable implements IFolderConfiguration {
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
private readonly cachedFolderPath: string;
private readonly cachedConfigurationPath: string;
private configurationModel: ConfigurationModel;
private readonly key: Thenable<ConfigurationKey>;
loaded: boolean = false;
constructor(
folder: URI,
configFolderRelativePath: string,
environmentService: IEnvironmentService) {
private readonly configurationCache: IConfigurationCache
) {
super();
this.cachedFolderPath = join(environmentService.userDataPath, 'CachedConfigurations', 'folders', createHash('md5').update(join(folder.path, configFolderRelativePath)).digest('hex'));
this.cachedConfigurationPath = join(this.cachedFolderPath, 'configuration.json');
this.key = createSHA1(join(folder.path, configFolderRelativePath)).then(key => (<ConfigurationKey>{ type: 'folder', key }));
this.configurationModel = new ConfigurationModel();
}
loadConfiguration(): Promise<ConfigurationModel> {
return pfs.readFile(this.cachedConfigurationPath)
.then(contents => {
const parsed: IConfigurationModel = JSON.parse(contents.toString());
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
this.loaded = true;
return this.configurationModel;
}, () => this.configurationModel);
async loadConfiguration(): Promise<ConfigurationModel> {
try {
const key = await this.key;
const contents = await this.configurationCache.read(key);
const parsed: IConfigurationModel = JSON.parse(contents.toString());
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
this.loaded = true;
} catch (e) {
}
return this.configurationModel;
}
updateConfiguration(configurationModel: ConfigurationModel): Promise<void> {
const raw = JSON.stringify(configurationModel.toJSON());
return this.createCachedFolder().then(created => {
if (created) {
return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath);
}
return undefined;
});
async updateConfiguration(configurationModel: ConfigurationModel): Promise<void> {
const key = await this.key;
if (configurationModel.keys.length) {
await this.configurationCache.write(key, JSON.stringify(configurationModel.toJSON()));
} else {
await this.configurationCache.remove(key);
}
}
reprocess(): ConfigurationModel {
@@ -751,12 +824,6 @@ export class CachedFolderConfiguration extends Disposable implements IFolderConf
getUnsupportedKeys(): string[] {
return [];
}
private createCachedFolder(): Promise<boolean> {
return Promise.resolve(pfs.exists(this.cachedFolderPath))
.then(undefined, () => false)
.then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false));
}
}
export class FolderConfiguration extends Disposable implements IFolderConfiguration {
@@ -765,24 +832,27 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
readonly onDidChange: Event<void> = this._onDidChange.event;
private folderConfiguration: IFolderConfiguration;
private readonly configurationFolder: URI;
private cachedFolderConfiguration: CachedFolderConfiguration;
private _loaded: boolean = false;
constructor(
readonly workspaceFolder: IWorkspaceFolder,
private readonly configFolderRelativePath: string,
configFolderRelativePath: string,
private readonly workbenchState: WorkbenchState,
private environmentService: IEnvironmentService,
configurationFileService: IConfigurationFileService,
configurationCache: IConfigurationCache,
fileService?: IFileService
) {
super();
this.cachedFolderConfiguration = new CachedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.environmentService);
this.configurationFolder = resources.joinPath(workspaceFolder.uri, configFolderRelativePath);
this.cachedFolderConfiguration = new CachedFolderConfiguration(workspaceFolder.uri, configFolderRelativePath, configurationCache);
this.folderConfiguration = this.cachedFolderConfiguration;
if (fileService) {
this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService);
} else if (this.workspaceFolder.uri.scheme === Schemas.file) {
this.folderConfiguration = new NodeBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState);
this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService);
} else if (workspaceFolder.uri.scheme === Schemas.file) {
this.folderConfiguration = new NodeBasedFolderConfiguration(configurationFileService, this.configurationFolder, this.workbenchState);
}
this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
}
@@ -817,7 +887,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
}
private adoptFromCachedConfiguration(fileService: IFileService): Promise<boolean> {
const folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService);
const folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService);
return folderConfiguration.loadConfiguration()
.then(() => {
this.folderConfiguration = folderConfiguration;
@@ -829,7 +899,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
private adoptFromNodeBasedConfiguration(fileService: IFileService): Promise<boolean> {
const oldFolderConfiguration = this.folderConfiguration;
this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService, <AbstractFolderConfiguration>oldFolderConfiguration);
this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService, <AbstractFolderConfiguration>oldFolderConfiguration);
oldFolderConfiguration.dispose();
this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
return Promise.resolve(false);
@@ -841,7 +911,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
}
private updateCache(): Promise<void> {
if (this.workspaceFolder.uri.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) {
if (this.configurationFolder.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) {
return this.folderConfiguration.loadConfiguration()
.then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel));
}

View File

@@ -10,25 +10,20 @@ import { ResourceMap } from 'vs/base/common/map';
import { equals, deepClone } from 'vs/base/common/objects';
import { Disposable } from 'vs/base/common/lifecycle';
import { Queue, Barrier } from 'vs/base/common/async';
import { writeFile } from 'vs/base/node/pfs';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { isLinux } from 'vs/base/common/platform';
import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels';
import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration';
import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import product from 'vs/platform/product/node/product';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/common/configurationEditingService';
import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, LocalUserConfiguration } from 'vs/workbench/services/configuration/node/configuration';
import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, LocalUserConfiguration } from 'vs/workbench/services/configuration/browser/configuration';
import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
@@ -36,7 +31,6 @@ import { isEqual, dirname } from 'vs/base/common/resources';
import { mark } from 'vs/base/common/performance';
import { Schemas } from 'vs/base/common/network';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService {
@@ -44,9 +38,10 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
private workspace: Workspace;
private completeWorkspaceBarrier: Barrier;
private readonly configurationCache: IConfigurationCache;
private _configuration: Configuration;
private defaultConfiguration: DefaultConfigurationModel;
private localUserConfiguration: LocalUserConfiguration;
private localUserConfiguration: LocalUserConfiguration | null = null;
private remoteUserConfiguration: RemoteUserConfiguration | null = null;
private workspaceConfiguration: WorkspaceConfiguration;
private cachedFolderConfigs: ResourceMap<FolderConfiguration>;
@@ -69,18 +64,25 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
private configurationEditingService: ConfigurationEditingService;
private jsonEditingService: JSONEditingService;
constructor(configuration: IWindowConfiguration, private environmentService: IEnvironmentService, private remoteAgentService: IRemoteAgentService, private workspaceSettingsRootFolder: string = FOLDER_CONFIG_FOLDER_NAME) {
constructor(
{ userSettingsResource, remoteAuthority, configurationCache }: { userSettingsResource?: URI, remoteAuthority?: string, configurationCache: IConfigurationCache },
private readonly configurationFileService: IConfigurationFileService,
private readonly remoteAgentService: IRemoteAgentService,
) {
super();
this.completeWorkspaceBarrier = new Barrier();
this.defaultConfiguration = new DefaultConfigurationModel();
this.localUserConfiguration = this._register(new LocalUserConfiguration(environmentService));
this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration)));
if (configuration.remoteAuthority) {
this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(configuration.remoteAuthority, environmentService));
this.configurationCache = configurationCache;
if (userSettingsResource) {
this.localUserConfiguration = this._register(new LocalUserConfiguration(userSettingsResource, configurationFileService));
this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration)));
}
if (remoteAuthority) {
this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache));
this._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration)));
}
this.workspaceConfiguration = this._register(new WorkspaceConfiguration(environmentService));
this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, this.configurationFileService));
this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas()));
@@ -284,10 +286,10 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
return this._configuration.keys();
}
initialize(arg: IWorkspaceInitializationPayload, postInitialisationTask: () => void = () => null): Promise<any> {
initialize(arg: IWorkspaceInitializationPayload): Promise<any> {
mark('willInitWorkspaceService');
return this.createWorkspace(arg)
.then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace, postInitialisationTask)).then(() => {
.then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace)).then(() => {
mark('didInitWorkspaceService');
});
}
@@ -295,7 +297,14 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
acquireFileService(fileService: IFileService): void {
this.fileService = fileService;
const changedWorkspaceFolders: IWorkspaceFolder[] = [];
this.localUserConfiguration.adopt(fileService);
if (this.localUserConfiguration) {
this.localUserConfiguration.adopt(fileService)
.then(changedModel => {
if (changedModel) {
this.onLocalUserConfigurationChanged(changedModel);
}
});
}
Promise.all([this.workspaceConfiguration.adopt(fileService), ...this.cachedFolderConfigs.values()
.map(folderConfiguration => folderConfiguration.adopt(fileService)
.then(result => {
@@ -382,7 +391,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
}
}
private updateWorkspaceAndInitializeConfiguration(workspace: Workspace, postInitialisationTask: () => void): Promise<void> {
private updateWorkspaceAndInitializeConfiguration(workspace: Workspace): Promise<void> {
const hasWorkspaceBefore = !!this.workspace;
let previousState: WorkbenchState;
let previousWorkspacePath: string | undefined;
@@ -399,8 +408,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
return this.initializeConfiguration().then(() => {
postInitialisationTask(); // Post initialisation task should be run before triggering events.
// Trigger changes after configuration initialization so that configuration is up to date.
if (hasWorkspaceBefore) {
const newState = this.getWorkbenchState();
@@ -446,12 +453,12 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
}
private initializeUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> {
return Promise.all([this.localUserConfiguration.initialize(), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())])
return Promise.all([this.localUserConfiguration ? this.localUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel()), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())])
.then(([local, remote]) => ({ local, remote }));
}
private reloadUserConfiguration(key?: string): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> {
return Promise.all([this.localUserConfiguration.reload(), this.remoteUserConfiguration ? this.remoteUserConfiguration.reload() : Promise.resolve(new ConfigurationModel())])
return Promise.all([this.localUserConfiguration ? this.localUserConfiguration.reload() : Promise.resolve(new ConfigurationModel()), this.remoteUserConfiguration ? this.remoteUserConfiguration.reload() : Promise.resolve(new ConfigurationModel())])
.then(([local, remote]) => ({ local, remote }));
}
@@ -622,7 +629,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
return Promise.all([...folders.map(folder => {
let folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (!folderConfiguration) {
folderConfiguration = new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState(), this.environmentService, this.fileService);
folderConfiguration = new FolderConfiguration(folder, FOLDER_CONFIG_FOLDER_NAME, this.getWorkbenchState(), this.configurationFileService, this.configurationCache, this.fileService);
this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder)));
this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration));
}
@@ -709,99 +716,4 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
}
return {};
}
}
interface IExportedConfigurationNode {
name: string;
description: string;
default: any;
type?: string | string[];
enum?: any[];
enumDescriptions?: string[];
}
interface IConfigurationExport {
settings: IExportedConfigurationNode[];
buildTime: number;
commit?: string;
buildNumber?: number;
}
export class DefaultConfigurationExportHelper {
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IExtensionService private readonly extensionService: IExtensionService,
@ICommandService private readonly commandService: ICommandService) {
if (environmentService.args['export-default-configuration']) {
this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']);
}
}
private writeConfigModelAndQuit(targetPath: string): Promise<void> {
return Promise.resolve(this.extensionService.whenInstalledExtensionsRegistered())
.then(() => this.writeConfigModel(targetPath))
.then(() => this.commandService.executeCommand('workbench.action.quit'))
.then(() => { });
}
private writeConfigModel(targetPath: string): Promise<void> {
const config = this.getConfigModel();
const resultString = JSON.stringify(config, undefined, ' ');
return writeFile(targetPath, resultString);
}
private getConfigModel(): IConfigurationExport {
const configRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
const configurations = configRegistry.getConfigurations().slice();
const settings: IExportedConfigurationNode[] = [];
const processProperty = (name: string, prop: IConfigurationPropertySchema) => {
const propDetails: IExportedConfigurationNode = {
name,
description: prop.description || prop.markdownDescription || '',
default: prop.default,
type: prop.type
};
if (prop.enum) {
propDetails.enum = prop.enum;
}
if (prop.enumDescriptions || prop.markdownEnumDescriptions) {
propDetails.enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions;
}
settings.push(propDetails);
};
const processConfig = (config: IConfigurationNode) => {
if (config.properties) {
for (let name in config.properties) {
processProperty(name, config.properties[name]);
}
}
if (config.allOf) {
config.allOf.forEach(processConfig);
}
};
configurations.forEach(processConfig);
const excludedProps = configRegistry.getExcludedConfigurationProperties();
for (let name in excludedProps) {
processProperty(name, excludedProps[name]);
}
const result: IConfigurationExport = {
settings: settings.sort((a, b) => a.name.localeCompare(b.name)),
buildTime: Date.now(),
commit: product.commit,
buildNumber: product.settingsSearchBuildId
};
return result;
}
}
}

View File

@@ -3,6 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
export const FOLDER_CONFIG_FOLDER_NAME = '.azuredatastudio';
export const FOLDER_SETTINGS_NAME = 'settings';
export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTINGS_NAME}.json`;
@@ -19,3 +21,19 @@ export const LAUNCH_CONFIGURATION_KEY = 'launch';
export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null);
WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${TASKS_CONFIGURATION_KEY}.json`;
WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${LAUNCH_CONFIGURATION_KEY}.json`;
export type ConfigurationKey = { type: 'user' | 'workspaces' | 'folder', key: string };
export interface IConfigurationCache {
read(key: ConfigurationKey): Promise<string>;
write(key: ConfigurationKey, content: string): Promise<void>;
remove(key: ConfigurationKey): Promise<void>;
}
export interface IConfigurationFileService {
exists(resource: URI): Promise<boolean>;
resolveContent(resource: URI): Promise<string>;
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as pfs from 'vs/base/node/pfs';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { join } from 'vs/base/common/path';
import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration';
export class ConfigurationCache implements IConfigurationCache {
private readonly cachedConfigurations: Map<string, CachedConfiguration> = new Map<string, CachedConfiguration>();
constructor(private readonly environmentService: IEnvironmentService) {
}
read(key: ConfigurationKey): Promise<string> {
return this.getCachedConfiguration(key).read();
}
write(key: ConfigurationKey, content: string): Promise<void> {
return this.getCachedConfiguration(key).save(content);
}
remove(key: ConfigurationKey): Promise<void> {
return this.getCachedConfiguration(key).remove();
}
private getCachedConfiguration({ type, key }: ConfigurationKey): CachedConfiguration {
const k = `${type}:${key}`;
let cachedConfiguration = this.cachedConfigurations.get(k);
if (!cachedConfiguration) {
cachedConfiguration = new CachedConfiguration({ type, key }, this.environmentService);
this.cachedConfigurations.set(k, cachedConfiguration);
}
return cachedConfiguration;
}
}
class CachedConfiguration {
private cachedConfigurationFolderPath: string;
private cachedConfigurationFilePath: string;
constructor(
{ type, key }: ConfigurationKey,
environmentService: IEnvironmentService
) {
this.cachedConfigurationFolderPath = join(environmentService.userDataPath, 'CachedConfigurations', type, key);
this.cachedConfigurationFilePath = join(this.cachedConfigurationFolderPath, type === 'workspaces' ? 'workspace.json' : 'configuration.json');
}
async read(): Promise<string> {
try {
const content = await pfs.readFile(this.cachedConfigurationFilePath);
return content.toString();
} catch (e) {
return '';
}
}
async save(content: string): Promise<void> {
const created = await this.createCachedFolder();
if (created) {
await pfs.writeFile(this.cachedConfigurationFilePath, content);
}
}
remove(): Promise<void> {
return pfs.rimraf(this.cachedConfigurationFolderPath);
}
private createCachedFolder(): Promise<boolean> {
return Promise.resolve(pfs.exists(this.cachedConfigurationFolderPath))
.then(undefined, () => false)
.then(exists => exists ? exists : pfs.mkdirp(this.cachedConfigurationFolderPath).then(() => true, () => false));
}
}

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { writeFile } from 'vs/base/node/pfs';
import product from 'vs/platform/product/node/product';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands';
interface IExportedConfigurationNode {
name: string;
description: string;
default: any;
type?: string | string[];
enum?: any[];
enumDescriptions?: string[];
}
interface IConfigurationExport {
settings: IExportedConfigurationNode[];
buildTime: number;
commit?: string;
buildNumber?: number;
}
export class DefaultConfigurationExportHelper {
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IExtensionService private readonly extensionService: IExtensionService,
@ICommandService private readonly commandService: ICommandService) {
if (environmentService.args['export-default-configuration']) {
this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']);
}
}
private writeConfigModelAndQuit(targetPath: string): Promise<void> {
return Promise.resolve(this.extensionService.whenInstalledExtensionsRegistered())
.then(() => this.writeConfigModel(targetPath))
.then(() => this.commandService.executeCommand('workbench.action.quit'))
.then(() => { });
}
private writeConfigModel(targetPath: string): Promise<void> {
const config = this.getConfigModel();
const resultString = JSON.stringify(config, undefined, ' ');
return writeFile(targetPath, resultString);
}
private getConfigModel(): IConfigurationExport {
const configRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
const configurations = configRegistry.getConfigurations().slice();
const settings: IExportedConfigurationNode[] = [];
const processProperty = (name: string, prop: IConfigurationPropertySchema) => {
const propDetails: IExportedConfigurationNode = {
name,
description: prop.description || prop.markdownDescription || '',
default: prop.default,
type: prop.type
};
if (prop.enum) {
propDetails.enum = prop.enum;
}
if (prop.enumDescriptions || prop.markdownEnumDescriptions) {
propDetails.enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions;
}
settings.push(propDetails);
};
const processConfig = (config: IConfigurationNode) => {
if (config.properties) {
for (let name in config.properties) {
processProperty(name, config.properties[name]);
}
}
if (config.allOf) {
config.allOf.forEach(processConfig);
}
};
configurations.forEach(processConfig);
const excludedProps = configRegistry.getExcludedConfigurationProperties();
for (let name in excludedProps) {
processProperty(name, excludedProps[name]);
}
const result: IConfigurationExport = {
settings: settings.sort((a, b) => a.name.localeCompare(b.name)),
buildTime: Date.now(),
commit: product.commit,
buildNumber: product.settingsSearchBuildId
};
return result;
}
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as pfs from 'vs/base/node/pfs';
import { IConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration';
import { URI } from 'vs/base/common/uri';
export class ConfigurationFileService implements IConfigurationFileService {
exists(resource: URI): Promise<boolean> {
return pfs.exists(resource.fsPath);
}
async resolveContent(resource: URI): Promise<string> {
const contents = await pfs.readFile(resource.fsPath);
return contents.toString();
}
}

View File

@@ -14,31 +14,33 @@ import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/
import { parseArgs } from 'vs/platform/environment/node/argv';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import * as extfs from 'vs/base/node/extfs';
import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import * as uuid from 'vs/base/common/uuid';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditingService';
import { IFileService } from 'vs/platform/files/common/files';
import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { mkdirp } from 'vs/base/node/pfs';
import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CommandService } from 'vs/workbench/services/commands/common/commandService';
import { URI } from 'vs/base/common/uri';
import { createHash } from 'crypto';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { NullLogService } from 'vs/platform/log/common/log';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
import { IFileService } from 'vs/platform/files/common/files';
import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache';
import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService';
class SettingsTestEnvironmentService extends EnvironmentService {
@@ -85,7 +87,7 @@ suite('ConfigurationEditingService', () => {
.then(() => setUpServices());
});
async function setUpWorkspace(): Promise<boolean> {
async function setUpWorkspace(): Promise<void> {
const id = uuid.generateUuid();
parentDir = path.join(os.tmpdir(), 'vsctests', id);
workspaceDir = path.join(parentDir, 'workspaceconfig', id);
@@ -105,11 +107,19 @@ suite('ConfigurationEditingService', () => {
instantiationService.stub(IEnvironmentService, environmentService);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, {});
instantiationService.stub(IRemoteAgentService, remoteAgentService);
const workspaceService = new WorkspaceService(<IWindowConfiguration>{}, environmentService, remoteAgentService);
const workspaceService = new WorkspaceService({ userSettingsResource: URI.file(environmentService.appSettingsPath), configurationCache: new ConfigurationCache(environmentService) }, new ConfigurationFileService(), remoteAgentService);
instantiationService.stub(IWorkspaceContextService, workspaceService);
return workspaceService.initialize(noWorkspace ? { id: '' } : { folder: URI.file(workspaceDir), id: createHash('md5').update(URI.file(workspaceDir).toString()).digest('hex') }).then(() => {
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }));
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
fileService.setLegacyService(new LegacyFileService(
fileService,
workspaceService,
TestEnvironmentService,
new TestTextResourceConfigurationService(),
));
instantiationService.stub(IFileService, fileService);
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(ICommandService, CommandService);
@@ -135,7 +145,7 @@ suite('ConfigurationEditingService', () => {
function clearWorkspace(): Promise<void> {
return new Promise<void>((c, e) => {
if (parentDir) {
extfs.del(parentDir, os.tmpdir(), () => c(undefined), () => c(undefined));
rimraf(parentDir, RimRafMode.MOVE).then(c, c);
} else {
c(undefined);
}

View File

@@ -16,15 +16,14 @@ import { parseArgs } from 'vs/platform/environment/node/argv';
import * as pfs from 'vs/base/node/pfs';
import * as uuid from 'vs/base/common/uuid';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
import { ISingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { ISingleFolderWorkspaceInitializationPayload, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditingService';
import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { ConfigurationTarget, IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
@@ -32,15 +31,18 @@ import { TextModelResolverService } from 'vs/workbench/services/textmodelResolve
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService';
import { createHash } from 'crypto';
import { Emitter, Event } from 'vs/base/common/event';
import { Schemas } from 'vs/base/common/network';
import { originalFSPath } from 'vs/base/common/resources';
import { isLinux } from 'vs/base/common/platform';
import { IWorkspaceIdentifier } from 'vs/workbench/services/configuration/node/configuration';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-browser/remoteAuthorityResolverService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { NullLogService } from 'vs/platform/log/common/log';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache';
import { ConfigurationFileService } from 'vs/workbench/services/configuration/node/configurationFileService';
class SettingsTestEnvironmentService extends EnvironmentService {
@@ -91,7 +93,7 @@ function setUpWorkspace(folders: string[]): Promise<{ parentDir: string, configP
suite('WorkspaceContextService - Folder', () => {
test('getWorkspace()', () => {
// {{SQL CARBON EDIT}} - Remove test
// {{SQL CARBON EDIT}} - Remove tests
assert.equal(0, 0);
});
});

View File

@@ -18,19 +18,35 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput, IPickOptions, Omit,
import { CancellationToken } from 'vs/base/common/cancellation';
import * as Types from 'vs/base/common/types';
import { IWindowService, IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { EditorType } from 'vs/editor/common/editorCommon';
import { Selection } from 'vs/editor/common/core/selection';
const mockLineNumber = 10;
class TestEditorServiceWithActiveEditor extends TestEditorService {
get activeTextEditorWidget(): any {
return {
getEditorType() {
return EditorType.ICodeEditor;
},
getSelection() {
return new Selection(mockLineNumber, 1, mockLineNumber, 10);
}
};
}
}
suite('Configuration Resolver Service', () => {
let configurationResolverService: IConfigurationResolverService | null;
let envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' };
let windowService: IWindowService;
let mockCommandService: MockCommandService;
let editorService: TestEditorService;
let editorService: TestEditorServiceWithActiveEditor;
let workspace: IWorkspaceFolder;
let quickInputService: MockQuickInputService;
setup(() => {
mockCommandService = new MockCommandService();
editorService = new TestEditorService();
editorService = new TestEditorServiceWithActiveEditor();
quickInputService = new MockQuickInputService();
windowService = new MockWindowService(envVariables);
workspace = {
@@ -58,14 +74,9 @@ suite('Configuration Resolver Service', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${workspaceRootFolderName} xyz'), 'abc workspaceLocation xyz');
});
// TODO@isidor mock the editor service properly
// test('current selected line number', () => {
// assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${editorService.mockLineNumber} xyz`);
// });
// test('current selected text', () => {
// assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${selectedText} xyz'), `abc ${editorService.mockSelectedText} xyz`);
// });
test('current selected line number', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, 'abc ${lineNumber} xyz'), `abc ${mockLineNumber} xyz`);
});
test('substitute many', () => {
if (platform.isWindows) {
@@ -123,7 +134,7 @@ suite('Configuration Resolver Service', () => {
}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');
});
@@ -140,7 +151,7 @@ suite('Configuration Resolver Service', () => {
}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz');
});
@@ -157,7 +168,7 @@ suite('Configuration Resolver Service', () => {
}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz');
} else {
@@ -178,7 +189,7 @@ suite('Configuration Resolver Service', () => {
}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2');
} else {
@@ -212,7 +223,7 @@ suite('Configuration Resolver Service', () => {
}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz');
});
@@ -222,7 +233,7 @@ suite('Configuration Resolver Service', () => {
editor: {}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz');
});
@@ -235,7 +246,7 @@ suite('Configuration Resolver Service', () => {
}
});
let service = new ConfigurationResolverService(windowService, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
let service = new ConfigurationResolverService(windowService, new TestEditorServiceWithActiveEditor(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService(), quickInputService);
assert.throws(() => service.resolve(workspace, 'abc ${env} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz'));

View File

@@ -125,7 +125,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
// Separator
if (entry instanceof Separator) {
return { type: 'separator' } as IContextMenuItem;
return { type: 'separator' };
}
// Submenu
@@ -133,7 +133,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
return {
label: unmnemonicLabel(entry.label),
submenu: this.createMenu(delegate, entry.entries, onHide)
} as IContextMenuItem;
};
}
// Normal Menu Item

View File

@@ -18,6 +18,7 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
export class FileDialogService implements IFileDialogService {
@@ -29,7 +30,8 @@ export class FileDialogService implements IFileDialogService {
@IHistoryService private readonly historyService: IHistoryService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService
) { }
defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
@@ -78,9 +80,7 @@ export class FileDialogService implements IFileDialogService {
return {
forceNewWindow: options.forceNewWindow,
telemetryExtraData: options.telemetryExtraData,
dialogOptions: {
defaultPath: options.defaultUri && options.defaultUri.fsPath
}
defaultPath: options.defaultUri && options.defaultUri.fsPath
};
}
@@ -103,7 +103,15 @@ export class FileDialogService implements IFileDialogService {
if (this.shouldUseSimplified(schema)) {
const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder');
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, !!options.forceNewWindow, true);
return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => {
if (uri) {
return (this.fileService.resolve(uri)).then(stat => {
const toOpen: IURIToOpen = stat.isDirectory ? { fileUri: uri } : { folderUri: uri };
return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow });
});
}
return undefined;
});
}
return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));
@@ -119,7 +127,12 @@ export class FileDialogService implements IFileDialogService {
if (this.shouldUseSimplified(schema)) {
const title = nls.localize('openFile.title', 'Open File');
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, !!options.forceNewWindow, true);
return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => {
if (uri) {
return this.windowService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow });
}
return undefined;
});
}
return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options));
@@ -135,7 +148,12 @@ export class FileDialogService implements IFileDialogService {
if (this.shouldUseSimplified(schema)) {
const title = nls.localize('openFolder.title', 'Open Folder');
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
return this.pickRemoteResourceAndOpen({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }, !!options.forceNewWindow, false);
return this.pickRemoteResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems }).then(uri => {
if (uri) {
return this.windowService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow });
}
return undefined;
});
}
return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options));
@@ -152,7 +170,12 @@ export class FileDialogService implements IFileDialogService {
const title = nls.localize('openWorkspace.title', 'Open Workspace');
const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }];
const availableFileSystems = this.ensureFileSchema(schema); // always allow file as well
return this.pickRemoteResourceAndOpen({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }, !!options.forceNewWindow, false);
return this.pickRemoteResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems }).then(uri => {
if (uri) {
return this.windowService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow });
}
return undefined;
});
}
return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
@@ -191,8 +214,8 @@ export class FileDialogService implements IFileDialogService {
if (!options.availableFileSystems) {
options.availableFileSystems = [schema]; // by default only allow loading in the own file system
}
return this.pickRemoteResource(options).then(urisToOpen => {
return urisToOpen && urisToOpen.map(uto => uto.uri);
return this.pickRemoteResource(options).then(uri => {
return uri ? [uri] : undefined;
});
}
@@ -223,16 +246,7 @@ export class FileDialogService implements IFileDialogService {
return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined);
}
private pickRemoteResourceAndOpen(options: IOpenDialogOptions, forceNewWindow: boolean, forceOpenWorkspaceAsFile: boolean) {
return this.pickRemoteResource(options).then(urisToOpen => {
if (urisToOpen) {
return this.windowService.openWindow(urisToOpen, { forceNewWindow, forceOpenWorkspaceAsFile });
}
return undefined;
});
}
private pickRemoteResource(options: IOpenDialogOptions): Promise<IURIToOpen[] | undefined> {
private pickRemoteResource(options: IOpenDialogOptions): Promise<URI | undefined> {
const remoteFileDialog = this.instantiationService.createInstance(RemoteFileDialog);
return remoteFileDialog.showOpenDialog(options);
}

View File

@@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri';
import { isWindows } from 'vs/base/common/platform';
import { ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { IWindowService, IURIToOpen, FileFilter } from 'vs/platform/windows/common/windows';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -40,7 +40,6 @@ export class RemoteFileDialog {
private options: IOpenDialogOptions;
private currentFolder: URI;
private filePickBox: IQuickPick<FileQuickPickItem>;
private filters: FileFilter[] | undefined;
private hidden: boolean;
private allowFileSelection: boolean;
private allowFolderSelection: boolean;
@@ -71,7 +70,7 @@ export class RemoteFileDialog {
this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService);
}
public async showOpenDialog(options: IOpenDialogOptions = {}): Promise<IURIToOpen[] | undefined> {
public async showOpenDialog(options: IOpenDialogOptions = {}): Promise<URI | undefined> {
this.scheme = this.getScheme(options.defaultUri, options.availableFileSystems);
const newOptions = await this.getOptions(options);
if (!newOptions) {
@@ -85,14 +84,7 @@ export class RemoteFileDialog {
let fallbackLabel = options.canSelectFiles ? (options.canSelectFolders ? openFileFolderString : openFileString) : openFolderString;
this.fallbackListItem = this.getFallbackFileSystem(fallbackLabel);
return this.pickResource().then(async fileFolderUri => {
if (fileFolderUri) {
const stat = await this.fileService.resolve(fileFolderUri);
return <IURIToOpen[]>[{ uri: fileFolderUri, typeHint: stat.isDirectory ? 'folder' : 'file' }];
}
return Promise.resolve(undefined);
});
return this.pickResource();
}
public async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
@@ -195,7 +187,7 @@ export class RemoteFileDialog {
this.filePickBox.buttons = [this.acceptButton];
this.filePickBox.onDidTriggerButton(_ => {
// accept button
const resolveValue = this.remoteUriFrom(this.filePickBox.value);
const resolveValue = this.addPostfix(this.remoteUriFrom(this.filePickBox.value));
this.validate(resolveValue).then(validated => {
if (validated) {
isResolving = true;
@@ -323,6 +315,7 @@ export class RemoteFileDialog {
}
if (resolveValue) {
resolveValue = this.addPostfix(resolveValue);
if (await this.validate(resolveValue)) {
return Promise.resolve(resolveValue);
}
@@ -387,6 +380,32 @@ export class RemoteFileDialog {
}
}
private addPostfix(uri: URI): URI {
let result = uri;
if (this.requiresTrailing && this.options.filters && this.options.filters.length > 0) {
// Make sure that the suffix is added. If the user deleted it, we automatically add it here
let hasExt: boolean = false;
const currentExt = resources.extname(uri).substr(1);
if (currentExt !== '') {
for (let i = 0; i < this.options.filters.length; i++) {
for (let j = 0; j < this.options.filters[i].extensions.length; j++) {
if ((this.options.filters[i].extensions[j] === '*') || (this.options.filters[i].extensions[j] === currentExt)) {
hasExt = true;
break;
}
}
if (hasExt) {
break;
}
}
}
if (!hasExt) {
result = resources.joinPath(resources.dirname(uri), resources.basename(uri) + '.' + this.options.filters[0].extensions[0]);
}
}
return result;
}
private async validate(uri: URI): Promise<boolean> {
let stat: IFileStat | undefined;
let statDirname: IFileStat | undefined;
@@ -545,11 +564,11 @@ export class RemoteFileDialog {
}
private filterFile(file: URI): boolean {
if (this.filters) {
if (this.options.filters) {
const ext = resources.extname(file);
for (let i = 0; i < this.filters.length; i++) {
for (let j = 0; j < this.filters[i].extensions.length; j++) {
if (ext === ('.' + this.filters[i].extensions[j])) {
for (let i = 0; i < this.options.filters.length; i++) {
for (let j = 0; j < this.options.filters[i].extensions.length; j++) {
if (ext === ('.' + this.options.filters[i].extensions[j])) {
return true;
}
}

View File

@@ -87,7 +87,7 @@ class NativeDialogService implements IDialogService {
return {
confirmed: buttonIndexMap[result.button] === 0 ? true : false,
checkboxChecked: result.checkboxChecked
} as IConfirmationResult;
};
});
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IResourceInput, ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
@@ -37,7 +37,7 @@ type ICachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditor
export class EditorService extends Disposable implements EditorServiceImpl {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<any>;
private static CACHE: ResourceMap<ICachedEditorInput> = new ResourceMap<ICachedEditorInput>();

View File

@@ -45,6 +45,7 @@ export interface IVisibleEditor extends IEditor {
}
export interface IEditorService {
_serviceBrand: ServiceIdentifier<any>;
/**

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { IRemoteConsoleLog } from 'vs/base/common/console';
export const IExtensionHostDebugService = createDecorator<IExtensionHostDebugService>('extensionHostDebugService');
export interface IExtensionHostDebugService {
_serviceBrand: any;
reload(resource: URI): void;
onReload: Event<URI>;
close(resource: URI): void;
onClose: Event<URI>;
attachSession(id: string, port: number): void;
onAttachSession: Event<{ id: string, port: number }>;
logToSession(id: string, log: IRemoteConsoleLog): void;
onLogToSession: Event<{ id: string, log: IRemoteConsoleLog }>;
terminateSession(id: string): void;
onTerminateSession: Event<string>;
}

View File

@@ -134,7 +134,7 @@ export class CachedExtensionScanner {
}
try {
await pfs.del(cacheFile);
await pfs.rimraf(cacheFile, pfs.RimRafMode.MOVE);
} catch (err) {
errors.onUnexpectedError(err);
console.error(err);

View File

@@ -21,9 +21,7 @@ import { findFreePort, randomPort } from 'vs/base/node/ports';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
import { generateRandomPipeName, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { IBroadcast, IBroadcastService } from 'vs/workbench/services/broadcast/common/broadcast';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
import { ILabelService } from 'vs/platform/label/common/label';
import { ILifecycleService, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
@@ -38,6 +36,7 @@ import { withNullAsUndefined } from 'vs/base/common/types';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { parseExtensionDevOptions } from '../common/extensionDevOptions';
import { VSBuffer } from 'vs/base/common/buffer';
import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug';
export interface IExtensionHostStarter {
readonly onCrashed: Event<[number, string | null]>;
@@ -77,12 +76,12 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
@INotificationService private readonly _notificationService: INotificationService,
@IWindowsService private readonly _windowsService: IWindowsService,
@IWindowService private readonly _windowService: IWindowService,
@IBroadcastService private readonly _broadcastService: IBroadcastService,
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ILogService private readonly _logService: ILogService,
@ILabelService private readonly _labelService: ILabelService
@ILabelService private readonly _labelService: ILabelService,
@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService
) {
const devOpts = parseExtensionDevOptions(this._environmentService);
this._isExtensionDevHost = devOpts.isExtensionDevHost;
@@ -102,7 +101,16 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
this._toDispose.push(this._onCrashed);
this._toDispose.push(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));
this._toDispose.push(this._lifecycleService.onShutdown(reason => this.terminate()));
this._toDispose.push(this._broadcastService.onBroadcast(b => this._onBroadcast(b)));
this._toDispose.push(this._extensionHostDebugService.onClose(resource => {
if (this._isExtensionDevHost && isEqual(resource, this._environmentService.extensionDevelopmentLocationURI)) {
this._windowService.closeWindow();
}
}));
this._toDispose.push(this._extensionHostDebugService.onReload(resource => {
if (this._isExtensionDevHost && isEqual(resource, this._environmentService.extensionDevelopmentLocationURI)) {
this._windowService.reloadWindow();
}
}));
const globalExitListener = () => this.terminate();
process.once('exit', globalExitListener);
@@ -115,24 +123,6 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
this.terminate();
}
private _onBroadcast(broadcast: IBroadcast): void {
// Close Ext Host Window Request
if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL && this._isExtensionDevHost) {
const extensionLocations = broadcast.payload as string[];
if (Array.isArray(extensionLocations) && extensionLocations.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) {
this._windowService.closeWindow();
}
}
if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) {
const extensionPaths = broadcast.payload as string[];
if (Array.isArray(extensionPaths) && extensionPaths.some(uriString => isEqual(this._environmentService.extensionDevelopmentLocationURI, URI.parse(uriString)))) {
this._windowService.reloadWindow();
}
}
}
public start(): Promise<IMessagePassingProtocol> | null {
if (this._terminating) {
// .terminate() was called
@@ -230,14 +220,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal));
// Notify debugger that we are ready to attach to the process if we run a development extension
if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug) {
this._broadcastService.broadcast({
channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
payload: {
debugId: this._environmentService.debugExtensionHost.debugId,
port: portData.actual
}
});
if (this._isExtensionDevHost && portData.actual && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {
this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portData.actual);
}
this._inspectPort = portData.actual;
@@ -445,14 +429,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
}
// Broadcast to other windows if we are in development mode
else if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
this._broadcastService.broadcast({
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
payload: {
logEntry: entry,
debugId: this._environmentService.debugExtensionHost.debugId
}
});
else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) {
this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry);
}
}
@@ -557,14 +535,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
// If the extension development host was started without debugger attached we need
// to communicate this back to the main side to terminate the debug session
if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) {
this._broadcastService.broadcast({
channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
payload: {
debugId: this._environmentService.debugExtensionHost.debugId
}
});
if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) {
this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId);
event.join(timeout(100 /* wait a bit for IPC to get delivered */));
}
}

View File

@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionHostDebugService } from 'vs/workbench/services/extensions/common/extensionHostDebug';
import { URI } from 'vs/base/common/uri';
import { IRemoteConsoleLog } from 'vs/base/common/console';
import { ipcRenderer as ipc } from 'electron';
interface IReloadBroadcast {
type: 'vscode:extensionReload';
resource: string;
}
interface IAttachSessionBroadcast {
type: 'vscode:extensionAttach';
id: string;
port: number;
}
interface ICloseBroadcast {
type: 'vscode:extensionCloseExtensionHost';
resource: string;
}
interface ILogToSessionBroadcast {
type: 'vscode:extensionLog';
id: string;
log: IRemoteConsoleLog;
}
interface ITerminateSessionBroadcast {
type: 'vscode:extensionTerminate';
id: string;
}
const CHANNEL = 'vscode:extensionHostDebug';
class ExtensionHostDebugService implements IExtensionHostDebugService {
_serviceBrand: any;
private windowId: number;
private readonly _onReload = new Emitter<URI>();
private readonly _onClose = new Emitter<URI>();
private readonly _onAttachSession = new Emitter<{ id: string, port: number }>();
private readonly _onLogToSession = new Emitter<{ id: string, log: IRemoteConsoleLog }>();
private readonly _onTerminateSession = new Emitter<string>();
constructor(
@IWindowService readonly windowService: IWindowService,
) {
this.windowId = windowService.getCurrentWindowId();
ipc.on(CHANNEL, (_: unknown, broadcast: IReloadBroadcast | ICloseBroadcast | IAttachSessionBroadcast | ILogToSessionBroadcast | ITerminateSessionBroadcast) => {
if (broadcast.type === 'vscode:extensionReload') {
this._onReload.fire(URI.parse(broadcast.resource));
}
if (broadcast.type === 'vscode:extensionCloseExtensionHost') {
this._onClose.fire(URI.parse(broadcast.resource));
}
if (broadcast.type === 'vscode:extensionAttach') {
this._onAttachSession.fire({ id: broadcast.id, port: broadcast.port });
}
if (broadcast.type === 'vscode:extensionLog') {
this._onLogToSession.fire({ id: broadcast.id, log: broadcast.log });
}
if (broadcast.type === 'vscode:extensionTerminate') {
this._onTerminateSession.fire(broadcast.id);
}
});
}
reload(resource: URI): void {
ipc.send(CHANNEL, this.windowId, <IReloadBroadcast>{
type: 'vscode:extensionReload',
resource: resource.toString()
});
}
get onReload(): Event<URI> {
return this._onReload.event;
}
close(resource: URI): void {
ipc.send(CHANNEL, this.windowId, <ICloseBroadcast>{
type: 'vscode:extensionCloseExtensionHost',
resource: resource.toString()
});
}
get onClose(): Event<URI> {
return this._onClose.event;
}
attachSession(id: string, port: number): void {
ipc.send(CHANNEL, this.windowId, <IAttachSessionBroadcast>{
type: 'vscode:extensionAttach',
id,
port
});
}
get onAttachSession(): Event<{ id: string, port: number }> {
return this._onAttachSession.event;
}
logToSession(id: string, log: IRemoteConsoleLog): void {
ipc.send(CHANNEL, this.windowId, <ILogToSessionBroadcast>{
type: 'vscode:extensionLog',
id,
log
});
}
get onLogToSession(): Event<{ id: string, log: IRemoteConsoleLog }> {
return this._onLogToSession.event;
}
terminateSession(id: string): void {
ipc.send(CHANNEL, this.windowId, <ITerminateSessionBroadcast>{
type: 'vscode:extensionTerminate',
id
});
}
get onTerminateSession(): Event<string> {
return this._onTerminateSession.event;
}
}
registerSingleton(IExtensionHostDebugService, ExtensionHostDebugService, true);

View File

@@ -5,7 +5,7 @@
import { Profile, ProfileNode } from 'v8-inspect-profiler';
import { TernarySearchTree } from 'vs/base/common/map';
import { realpathSync } from 'vs/base/node/extfs';
import { realpathSync } from 'vs/base/node/extpath';
import { IExtensionHostProfile, IExtensionService, ProfileSegmentId, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';

View File

@@ -5,7 +5,7 @@
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import * as encoding from 'vs/base/node/encoding';
import { URI as uri } from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { IResolveContentOptions, isParent, IResourceEncodings, IResourceEncoding } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { extname } from 'vs/base/common/path';
@@ -16,7 +16,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { joinPath } from 'vs/base/common/resources';
export interface IEncodingOverride {
parent?: uri;
parent?: URI;
extension?: string;
encoding: string;
}
@@ -49,7 +49,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings
}));
}
getReadEncoding(resource: uri, options: IResolveContentOptions | undefined, detected: encoding.IDetectedEncodingResult): string {
getReadEncoding(resource: URI, options: IResolveContentOptions | undefined, detected: encoding.IDetectedEncodingResult): string {
let preferredEncoding: string | undefined;
// Encoding passed in as option
@@ -78,7 +78,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings
return this.getEncodingForResource(resource, preferredEncoding);
}
getWriteEncoding(resource: uri, preferredEncoding?: string): IResourceEncoding {
getWriteEncoding(resource: URI, preferredEncoding?: string): IResourceEncoding {
const resourceEncoding = this.getEncodingForResource(resource, preferredEncoding);
return {
@@ -87,7 +87,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings
};
}
private getEncodingForResource(resource: uri, preferredEncoding?: string): string {
private getEncodingForResource(resource: URI, preferredEncoding?: string): string {
let fileEncoding: string;
const override = this.getEncodingOverride(resource);
@@ -110,7 +110,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings
const encodingOverride: IEncodingOverride[] = [];
// Global settings
encodingOverride.push({ parent: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 });
encodingOverride.push({ parent: URI.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 });
// Workspace files
encodingOverride.push({ extension: WORKSPACE_EXTENSION, encoding: encoding.UTF8 });
@@ -124,7 +124,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings
return encodingOverride;
}
private getEncodingOverride(resource: uri): string | null {
private getEncodingOverride(resource: URI): string | null {
if (resource && this.encodingOverride && this.encodingOverride.length) {
for (const override of this.encodingOverride) {

View File

@@ -6,38 +6,23 @@
import * as paths from 'vs/base/common/path';
import * as fs from 'fs';
import * as os from 'os';
import * as crypto from 'crypto';
import * as assert from 'assert';
import { isParent, FileOperation, FileOperationEvent, IContent, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot, IFilesConfiguration, IFileSystemProviderRegistrationEvent, IFileSystemProvider, ILegacyFileService, IFileStatWithMetadata, IFileService, IResolveMetadataFileOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { FileOperation, FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, ICreateFileOptions, IContentData, ITextSnapshot, ILegacyFileService, IFileStatWithMetadata, IFileService, IFileSystemProvider, etag } from 'vs/platform/files/common/files';
import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/fileConstants';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { ResourceMap } from 'vs/base/common/map';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import * as extfs from 'vs/base/node/extfs';
import { nfcall, ThrottledDelayer, timeout } from 'vs/base/common/async';
import { timeout } from 'vs/base/common/async';
import { URI as uri } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform';
import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import * as pfs from 'vs/base/node/pfs';
import { detectEncodingFromBuffer, decodeStream, detectEncodingByBOM, UTF8 } from 'vs/base/node/encoding';
import * as flow from 'vs/base/node/flow';
import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService';
import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { Event, Emitter } from 'vs/base/common/event';
import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { getBaseLabel } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { onUnexpectedError } from 'vs/base/common/errors';
import product from 'vs/platform/product/node/product';
import { IEncodingOverride, ResourceEncodings } from 'vs/workbench/services/files/node/encoding';
@@ -45,182 +30,37 @@ import { createReadableOfSnapshot } from 'vs/workbench/services/files/node/strea
import { withUndefinedAsNull } from 'vs/base/common/types';
export interface IFileServiceTestOptions {
disableWatcher?: boolean;
encodingOverride?: IEncodingOverride[];
}
export class FileService extends Disposable implements ILegacyFileService, IFileService {
export class LegacyFileService extends Disposable implements ILegacyFileService {
_serviceBrand: any;
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms)
private static readonly NET_VERSION_ERROR = 'System.MissingMethodException';
private static readonly NET_VERSION_ERROR_IGNORE_KEY = 'ignoreNetVersionError';
private static readonly ENOSPC_ERROR = 'ENOSPC';
private static readonly ENOSPC_ERROR_IGNORE_KEY = 'ignoreEnospcError';
protected readonly _onFileChanges: Emitter<FileChangesEvent> = this._register(new Emitter<FileChangesEvent>());
get onFileChanges(): Event<FileChangesEvent> { return this._onFileChanges.event; }
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { return Disposable.None; }
protected readonly _onAfterOperation: Emitter<FileOperationEvent> = this._register(new Emitter<FileOperationEvent>());
get onAfterOperation(): Event<FileOperationEvent> { return this._onAfterOperation.event; }
protected readonly _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());
get onDidChangeFileSystemProviderRegistrations(): Event<IFileSystemProviderRegistrationEvent> { return this._onDidChangeFileSystemProviderRegistrations.event; }
readonly onWillActivateFileSystemProvider = Event.None;
private activeWorkspaceFileChangeWatcher: IDisposable | null;
private activeFileChangesWatchers: ResourceMap<{ unwatch: Function, count: number }>;
private fileChangesWatchDelayer: ThrottledDelayer<void>;
private undeliveredRawFileChangesEvents: IRawFileChange[];
private _encoding: ResourceEncodings;
constructor(
private contextService: IWorkspaceContextService,
protected fileService: IFileService,
contextService: IWorkspaceContextService,
private environmentService: IEnvironmentService,
private textResourceConfigurationService: ITextResourceConfigurationService,
private configurationService: IConfigurationService,
private lifecycleService: ILifecycleService,
private storageService: IStorageService,
private notificationService: INotificationService,
private options: IFileServiceTestOptions = Object.create(null)
) {
super();
this.activeFileChangesWatchers = new ResourceMap<{ unwatch: Function, count: number }>();
this.fileChangesWatchDelayer = new ThrottledDelayer<void>(FileService.FS_EVENT_DELAY);
this.undeliveredRawFileChangesEvents = [];
this._encoding = new ResourceEncodings(textResourceConfigurationService, environmentService, contextService, this.options.encodingOverride);
this.registerListeners();
}
get encoding(): ResourceEncodings {
return this._encoding;
}
private registerListeners(): void {
// Wait until we are fully running before starting file watchers
this.lifecycleService.when(LifecyclePhase.Restored).then(() => {
this.setupFileWatching();
});
// Workbench State Change
this._register(this.contextService.onDidChangeWorkbenchState(() => {
if (this.lifecycleService.phase >= LifecyclePhase.Restored) {
this.setupFileWatching();
}
}));
// Lifecycle
this.lifecycleService.onShutdown(this.dispose, this);
}
private handleError(error: string | Error): void {
const msg = error ? error.toString() : undefined;
if (!msg) {
return;
}
// Forward to unexpected error handler
onUnexpectedError(msg);
// Detect if we run < .NET Framework 4.5 (TODO@ben remove with new watcher impl)
if (msg.indexOf(FileService.NET_VERSION_ERROR) >= 0 && !this.storageService.getBoolean(FileService.NET_VERSION_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) {
this.notificationService.prompt(
Severity.Warning,
nls.localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."),
[{
label: nls.localize('installNet', "Download .NET Framework 4.5"),
run: () => window.open('https://go.microsoft.com/fwlink/?LinkId=786533')
},
{
label: nls.localize('neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => this.storageService.store(FileService.NET_VERSION_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE)
}],
{ sticky: true }
);
}
// Detect if we run into ENOSPC issues
if (msg.indexOf(FileService.ENOSPC_ERROR) >= 0 && !this.storageService.getBoolean(FileService.ENOSPC_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) {
this.notificationService.prompt(
Severity.Warning,
nls.localize('enospcError', "{0} is unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue.", product.nameLong),
[{
label: nls.localize('learnMore', "Instructions"),
run: () => window.open('https://go.microsoft.com/fwlink/?linkid=867693')
},
{
label: nls.localize('neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => this.storageService.store(FileService.ENOSPC_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE)
}],
{ sticky: true }
);
}
}
private setupFileWatching(): void {
// dispose old if any
if (this.activeWorkspaceFileChangeWatcher) {
this.activeWorkspaceFileChangeWatcher.dispose();
}
// Return if not aplicable
const workbenchState = this.contextService.getWorkbenchState();
if (workbenchState === WorkbenchState.EMPTY || this.options.disableWatcher) {
return;
}
// new watcher: use it if setting tells us so or we run in multi-root environment
const configuration = this.configurationService.getValue<IFilesConfiguration>();
if ((configuration.files && configuration.files.useExperimentalFileWatcher) || workbenchState === WorkbenchState.WORKSPACE) {
const multiRootWatcher = new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose);
this.activeWorkspaceFileChangeWatcher = toDisposable(multiRootWatcher.startWatching());
}
// legacy watcher
else {
let watcherIgnoredPatterns: string[] = [];
if (configuration.files && configuration.files.watcherExclude) {
watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]);
}
if (isWindows) {
const legacyWindowsWatcher = new WindowsWatcherService(this.contextService, watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose);
this.activeWorkspaceFileChangeWatcher = toDisposable(legacyWindowsWatcher.startWatching());
} else {
const legacyUnixWatcher = new UnixWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose);
this.activeWorkspaceFileChangeWatcher = toDisposable(legacyUnixWatcher.startWatching());
}
}
}
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
return Disposable.None;
}
activateProvider(scheme: string): Promise<void> {
return Promise.reject(new Error('not implemented'));
}
canHandleResource(resource: uri): boolean {
return resource.scheme === Schemas.file;
}
hasCapability(resource: uri, capability: FileSystemProviderCapabilities): Promise<boolean> {
return Promise.resolve(false);
}
//#region Read File
resolveContent(resource: uri, options?: IResolveContentOptions): Promise<IContent> {
return this.resolveStreamContent(resource, options).then(streamContent => {
@@ -280,7 +120,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
return Promise.reject(error);
};
const statsPromise = this.resolve(resource).then(stat => {
const statsPromise = this.fileService.resolve(resource).then(stat => {
result.resource = stat.resource;
result.name = stat.name;
result.mtime = stat.mtime;
@@ -447,7 +287,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
if (fd) {
fs.close(fd, err => {
if (err) {
this.handleError(`resolveFileData#close(): ${err.toString()}`);
onUnexpectedError(`resolveFileData#close(): ${err.toString()}`);
}
});
}
@@ -538,6 +378,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
});
}
//#endregion
//#region File Writing
updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise<IFileStatWithMetadata> {
if (options.writeElevated) {
return this.doUpdateContentElevated(resource, value, options);
@@ -634,7 +478,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): Promise<IFileStat> {
// Configure encoding related options as needed
const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null);
const writeFileOptions: pfs.IWriteFileOptions = options ? options : Object.create(null);
if (addBOM || encodingToWrite !== UTF8) {
writeFileOptions.encoding = {
charset: encodingToWrite,
@@ -653,7 +497,7 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
return writeFilePromise.then(() => {
// resolve
return this.resolve(resource);
return this.fileService.resolve(resource);
});
}
@@ -695,16 +539,16 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
}).then(() => {
// 3.) delete temp file
return pfs.del(tmpPath, os.tmpdir()).then(() => {
return pfs.rimraf(tmpPath, pfs.RimRafMode.MOVE).then(() => {
// 4.) resolve again
return this.resolve(resource);
return this.fileService.resolve(resource);
});
});
});
}).then(undefined, error => {
if (this.environmentService.verbose) {
this.handleError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`);
onUnexpectedError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`);
}
if (!FileOperationError.isFileOperationError(error)) {
@@ -719,6 +563,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
});
}
//#endregion
//#region Create File
createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): Promise<IFileStatWithMetadata> {
const absolutePath = this.toAbsolutePath(resource);
@@ -750,6 +598,10 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
});
}
//#endregion
//#region Helpers
private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): Promise<boolean /* exists */> {
return pfs.exists(absolutePath).then(exists => {
if (exists) {
@@ -812,133 +664,6 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
));
}
move(source: uri, target: uri, overwrite?: boolean): Promise<IFileStatWithMetadata> {
return this.moveOrCopyFile(source, target, false, !!overwrite);
}
copy(source: uri, target: uri, overwrite?: boolean): Promise<IFileStatWithMetadata> {
return this.moveOrCopyFile(source, target, true, !!overwrite);
}
private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): Promise<IFileStatWithMetadata> {
const sourcePath = this.toAbsolutePath(source);
const targetPath = this.toAbsolutePath(target);
// 1.) move / copy
return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => {
// 2.) resolve
return this.doResolve(target, { resolveMetadata: true }).then(result => {
// Events (unless it was a no-op because paths are identical)
if (sourcePath !== targetPath) {
this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result));
}
return result;
});
});
}
private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): Promise<void> {
// 1.) validate operation
if (isParent(targetPath, sourcePath, !isLinux)) {
return Promise.reject(new Error('Unable to move/copy when source path is parent of target path'));
} else if (sourcePath === targetPath) {
return Promise.resolve(); // no-op but not an error
}
// 2.) check if target exists
return pfs.exists(targetPath).then(exists => {
const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase();
// Return early with conflict if target exists and we are not told to overwrite
if (exists && !isCaseRename && !overwrite) {
return Promise.reject(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT));
}
// 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file
let deleteTargetPromise: Promise<void> = Promise.resolve();
if (exists && !isCaseRename) {
if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) {
return Promise.reject(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case!
}
deleteTargetPromise = this.del(uri.file(targetPath), { recursive: true });
}
return deleteTargetPromise.then(() => {
// 4.) make sure parents exists
return pfs.mkdirp(paths.dirname(targetPath)).then(() => {
// 4.) copy/move
if (keepCopy) {
return nfcall(extfs.copy, sourcePath, targetPath);
} else {
return nfcall(extfs.mv, sourcePath, targetPath);
}
});
});
});
}
del(resource: uri, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
if (options && options.useTrash) {
return this.doMoveItemToTrash(resource);
}
return this.doDelete(resource, !!(options && options.recursive));
}
private doMoveItemToTrash(resource: uri): Promise<void> {
const absolutePath = resource.fsPath;
const shell = (require('electron') as any as Electron.RendererInterface).shell; // workaround for being able to run tests out of VSCode debugger
const result = shell.moveItemToTrash(absolutePath);
if (!result) {
return Promise.reject(new Error(isWindows ? nls.localize('binFailed', "Failed to move '{0}' to the recycle bin", paths.basename(absolutePath)) : nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath))));
}
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
return Promise.resolve();
}
private doDelete(resource: uri, recursive: boolean): Promise<void> {
const absolutePath = this.toAbsolutePath(resource);
let assertNonRecursiveDelete: Promise<void>;
if (!recursive) {
assertNonRecursiveDelete = pfs.stat(absolutePath).then(stat => {
if (!stat.isDirectory()) {
return undefined;
}
return pfs.readdir(absolutePath).then(children => {
if (children.length === 0) {
return undefined;
}
return Promise.reject(new Error(nls.localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", paths.basename(absolutePath))));
});
}, error => Promise.resolve() /* ignore errors */);
} else {
assertNonRecursiveDelete = Promise.resolve();
}
return assertNonRecursiveDelete.then(() => {
return pfs.del(absolutePath, os.tmpdir()).then(() => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
});
});
}
// Helpers
private toAbsolutePath(arg1: uri | IFileStat): string {
let resource: uri;
if (arg1 instanceof uri) {
@@ -952,347 +677,5 @@ export class FileService extends Disposable implements ILegacyFileService, IFile
return paths.normalize(resource.fsPath);
}
private doResolve(resource: uri, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
private doResolve(resource: uri, options?: IResolveFileOptions): Promise<IFileStat>;
private doResolve(resource: uri, options: IResolveFileOptions = Object.create(null)): Promise<IFileStat> {
return this.toStatResolver(resource).then(model => model.resolve(options));
}
private toStatResolver(resource: uri): Promise<StatResolver> {
const absolutePath = this.toAbsolutePath(resource);
return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => {
return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.environmentService.verbose ? err => this.handleError(err) : undefined);
});
}
watch(resource: uri): void {
assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`);
// Check for existing watcher first
const entry = this.activeFileChangesWatchers.get(resource);
if (entry) {
entry.count += 1;
return;
}
// Create or get watcher for provided path
const fsPath = resource.fsPath;
const fsName = paths.basename(resource.fsPath);
const watcherDisposable = extfs.watch(fsPath, (eventType: string, filename: string) => {
const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename');
// The file was either deleted or renamed. Many tools apply changes to files in an
// atomic way ("Atomic Save") by first renaming the file to a temporary name and then
// renaming it back to the original name. Our watcher will detect this as a rename
// and then stops to work on Mac and Linux because the watcher is applied to the
// inode and not the name. The fix is to detect this case and trying to watch the file
// again after a certain delay.
// In addition, we send out a delete event if after a timeout we detect that the file
// does indeed not exist anymore.
if (renamedOrDeleted) {
// Very important to dispose the watcher which now points to a stale inode
watcherDisposable.dispose();
this.activeFileChangesWatchers.delete(resource);
// Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save")
setTimeout(() => {
this.exists(resource).then(exists => {
// File still exists, so reapply the watcher
if (exists) {
this.watch(resource);
}
// File seems to be really gone, so emit a deleted event
else {
this.onRawFileChange({
type: FileChangeType.DELETED,
path: fsPath
});
}
});
}, FileService.FS_REWATCH_DELAY);
}
// Handle raw file change
this.onRawFileChange({
type: FileChangeType.UPDATED,
path: fsPath
});
}, (error: string) => this.handleError(error));
// Remember in map
this.activeFileChangesWatchers.set(resource, {
count: 1,
unwatch: () => watcherDisposable.dispose()
});
}
private onRawFileChange(event: IRawFileChange): void {
// add to bucket of undelivered events
this.undeliveredRawFileChangesEvents.push(event);
if (this.environmentService.verbose) {
console.log('%c[File Watcher (node.js)]%c', 'color: blue', 'color: black', `${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
// handle emit through delayer to accommodate for bulk changes
this.fileChangesWatchDelayer.trigger(() => {
const buffer = this.undeliveredRawFileChangesEvents;
this.undeliveredRawFileChangesEvents = [];
// Normalize
const normalizedEvents = normalize(buffer);
// Logging
if (this.environmentService.verbose) {
normalizedEvents.forEach(r => {
console.log('%c[File Watcher (node.js)]%c >> normalized', 'color: blue', 'color: black', `${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
});
}
// Emit
this._onFileChanges.fire(toFileChangesEvent(normalizedEvents));
return Promise.resolve();
});
}
unwatch(resource: uri): void {
const watcher = this.activeFileChangesWatchers.get(resource);
if (watcher && --watcher.count === 0) {
watcher.unwatch();
this.activeFileChangesWatchers.delete(resource);
}
}
dispose(): void {
super.dispose();
if (this.activeWorkspaceFileChangeWatcher) {
this.activeWorkspaceFileChangeWatcher.dispose();
this.activeWorkspaceFileChangeWatcher = null;
}
this.activeFileChangesWatchers.forEach(watcher => watcher.unwatch());
this.activeFileChangesWatchers.clear();
}
// Tests only
resolve(resource: uri, options?: IResolveFileOptions): Promise<IFileStat>;
resolve(resource: uri, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
resolve(resource: uri, options?: IResolveFileOptions): Promise<IFileStat> {
return this.doResolve(resource, options);
}
resolveAll(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]> {
return Promise.all(toResolve.map(resourceAndOptions => this.doResolve(resourceAndOptions.resource, resourceAndOptions.options)
.then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false }))));
}
createFolder(resource: uri): Promise<IFileStatWithMetadata> {
// 1.) Create folder
const absolutePath = this.toAbsolutePath(resource);
return pfs.mkdirp(absolutePath).then(() => {
// 2.) Resolve
return this.doResolve(resource, { resolveMetadata: true }).then(result => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result));
return result;
});
});
}
exists(resource: uri): Promise<boolean> {
return this.resolve(resource).then(() => true, () => false);
}
}
function etag(stat: fs.Stats): string;
function etag(size: number, mtime: number): string;
function etag(arg1: any, arg2?: any): string {
let size: number;
let mtime: number;
if (typeof arg2 === 'number') {
size = arg1;
mtime = arg2;
} else {
size = (<fs.Stats>arg1).size;
mtime = (<fs.Stats>arg1).mtime.getTime();
}
return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`;
}
export class StatResolver {
private name: string;
private etag: string;
constructor(
private resource: uri,
private isSymbolicLink: boolean,
private isDirectory: boolean,
private mtime: number,
private size: number,
private errorLogger?: (error: Error | string) => void
) {
assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`);
this.name = getBaseLabel(resource);
this.etag = etag(size, mtime);
}
resolve(options: IResolveFileOptions | undefined): Promise<IFileStat> {
// General Data
const fileStat: IFileStat = {
resource: this.resource,
isDirectory: this.isDirectory,
isSymbolicLink: this.isSymbolicLink,
isReadonly: false,
name: this.name,
etag: this.etag,
size: this.size,
mtime: this.mtime
};
// File Specific Data
if (!this.isDirectory) {
return Promise.resolve(fileStat);
}
// Directory Specific Data
else {
// Convert the paths from options.resolveTo to absolute paths
let absoluteTargetPaths: string[] | null = null;
if (options && options.resolveTo) {
absoluteTargetPaths = [];
for (const resource of options.resolveTo) {
absoluteTargetPaths.push(resource.fsPath);
}
}
return new Promise<IFileStat>(resolve => {
// Load children
this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, !!(options && options.resolveSingleChildDescendants), children => {
if (children) {
children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child)
}
fileStat.children = children || [];
resolve(fileStat);
});
});
}
}
private resolveChildren(absolutePath: string, absoluteTargetPaths: string[] | null, resolveSingleChildDescendants: boolean, callback: (children: IFileStat[] | null) => void): void {
extfs.readdir(absolutePath, (error: Error, files: string[]) => {
if (error) {
if (this.errorLogger) {
this.errorLogger(error);
}
return callback(null); // return - we might not have permissions to read the folder
}
// for each file in the folder
flow.parallel(files, (file: string, clb: (error: Error | null, children: IFileStat | null) => void) => {
const fileResource = uri.file(paths.resolve(absolutePath, file));
let fileStat: fs.Stats;
let isSymbolicLink = false;
const $this = this;
flow.sequence(
function onError(error: Error): void {
if ($this.errorLogger) {
$this.errorLogger(error);
}
clb(null, null); // return - we might not have permissions to read the folder or stat the file
},
function stat(this: any): void {
extfs.statLink(fileResource.fsPath, this);
},
function countChildren(this: any, statAndLink: extfs.IStatAndLink): void {
fileStat = statAndLink.stat;
isSymbolicLink = statAndLink.isSymbolicLink;
if (fileStat.isDirectory()) {
extfs.readdir(fileResource.fsPath, (error, result) => {
this(null, result ? result.length : 0);
});
} else {
this(null, 0);
}
},
function resolve(childCount: number): void {
const childStat: IFileStat = {
resource: fileResource,
isDirectory: fileStat.isDirectory(),
isSymbolicLink,
isReadonly: false,
name: file,
mtime: fileStat.mtime.getTime(),
etag: etag(fileStat),
size: fileStat.size
};
// Return early for files
if (!fileStat.isDirectory()) {
return clb(null, childStat);
}
// Handle Folder
let resolveFolderChildren = false;
if (files.length === 1 && resolveSingleChildDescendants) {
resolveFolderChildren = true;
} else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) {
resolveFolderChildren = true;
}
// Continue resolving children based on condition
if (resolveFolderChildren) {
$this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => {
if (children) {
children = arrays.coalesce(children); // we don't want those null children
}
childStat.children = children || [];
clb(null, childStat);
});
}
// Otherwise return result
else {
clb(null, childStat);
}
});
}, (errors, result) => {
callback(result);
});
});
}
//#endregion
}

View File

@@ -3,185 +3,38 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import * as resources from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node/encoding';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, IWatchOptions, FileType, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata, IResolveMetadataFileOptions, etag } from 'vs/platform/files/common/files';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IResolveContentOptions, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/node/streams';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
class TypeOnlyStat implements IStat {
constructor(readonly type: FileType) {
//
}
// todo@remote -> make a getter and warn when
// being used in development.
mtime: number = 0;
ctime: number = 0;
size: number = 0;
}
function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): Promise<IFileStat> {
const [resource, stat] = tuple;
const fileStat: IFileStat = {
resource,
name: resources.basename(resource),
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly),
mtime: stat.mtime,
size: stat.size,
etag: etag(stat.mtime, stat.size),
};
if (fileStat.isDirectory) {
if (recurse && recurse([resource, stat])) {
// dir -> resolve
return provider.readdir(resource).then(entries => {
// resolve children if requested
return Promise.all(entries.map(tuple => {
const [name, type] = tuple;
const childResource = resources.joinPath(resource, name);
return toIFileStat(provider, [childResource, new TypeOnlyStat(type)], recurse);
})).then(children => {
fileStat.children = children;
return fileStat;
});
});
}
}
// file or (un-resolved) dir
return Promise.resolve(fileStat);
}
export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], to?: URI[]): Promise<IFileStat> {
const trie = TernarySearchTree.forPaths<true>();
trie.set(tuple[0].toString(), true);
if (isNonEmptyArray(to)) {
to.forEach(uri => trie.set(uri.toString(), true));
}
return toIFileStat(provider, tuple, candidate => {
return Boolean(trie.findSuperstr(candidate[0].toString()) || trie.get(candidate[0].toString()));
});
}
class WorkspaceWatchLogic extends Disposable {
private _watches = new Map<string, URI>();
constructor(
private _fileService: RemoteFileService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
) {
super();
this._refresh();
this._register(this._contextService.onDidChangeWorkspaceFolders(e => {
for (const removed of e.removed) {
this._unwatchWorkspace(removed.uri);
}
for (const added of e.added) {
this._watchWorkspace(added.uri);
}
}));
this._register(this._contextService.onDidChangeWorkbenchState(e => {
this._refresh();
}));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files.watcherExclude')) {
this._refresh();
}
}));
}
dispose(): void {
this._unwatchWorkspaces();
super.dispose();
}
private _refresh(): void {
this._unwatchWorkspaces();
for (const folder of this._contextService.getWorkspace().folders) {
if (folder.uri.scheme !== Schemas.file) {
this._watchWorkspace(folder.uri);
}
}
}
private _watchWorkspace(resource: URI) {
let excludes: string[] = [];
let config = this._configurationService.getValue<IFilesConfiguration>({ resource });
if (config.files && config.files.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
excludes.push(key);
}
}
}
this._watches.set(resource.toString(), resource);
this._fileService.watch(resource, { recursive: true, excludes });
}
private _unwatchWorkspace(resource: URI) {
if (this._watches.has(resource.toString())) {
this._fileService.unwatch(resource);
this._watches.delete(resource.toString());
}
}
private _unwatchWorkspaces() {
this._watches.forEach(uri => this._fileService.unwatch(uri));
this._watches.clear();
}
}
export class RemoteFileService extends FileService {
export class LegacyRemoteFileService extends LegacyFileService {
private readonly _provider: Map<string, IFileSystemProvider>;
constructor(
@IFileService private readonly _fileService: IFileService,
@IStorageService storageService: IStorageService,
@IFileService fileService: IFileService,
@IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@ILifecycleService lifecycleService: ILifecycleService,
@INotificationService notificationService: INotificationService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
) {
super(
fileService,
contextService,
environmentService,
textResourceConfigurationService,
configurationService,
lifecycleService,
storageService,
notificationService
textResourceConfigurationService
);
this._provider = new Map<string, IFileSystemProvider>();
this._register(new WorkspaceWatchLogic(this, configurationService, contextService));
}
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
@@ -201,7 +54,6 @@ export class RemoteFileService extends FileService {
// --- stat
private _withProvider(resource: URI): Promise<IFileSystemProvider> {
if (!resources.isAbsolutePath(resource)) {
throw new FileOperationError(
localize('invalidPath', "The path of resource '{0}' must be absolute", resource.toString(true)),
@@ -210,7 +62,7 @@ export class RemoteFileService extends FileService {
}
return Promise.all([
this._fileService.activateProvider(resource.scheme)
this.fileService.activateProvider(resource.scheme)
]).then(() => {
const provider = this._provider.get(resource.scheme);
if (!provider) {
@@ -223,48 +75,13 @@ export class RemoteFileService extends FileService {
});
}
resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
resolve(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
if (resource.scheme === Schemas.file) {
return super.resolve(resource, options);
} else {
return this._doResolveFiles([{ resource, options }]).then(data => {
if (data.length !== 1 || !data[0].success) {
throw new FileOperationError(
localize('fileNotFoundError', "File not found ({0})", resource.toString(true)),
FileOperationResult.FILE_NOT_FOUND
);
} else {
return data[0].stat!;
}
});
}
}
private _doResolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
return this._withProvider(toResolve[0].resource).then(provider => {
let result: IResolveFileResult[] = [];
let promises = toResolve.map((item, idx) => {
return provider.stat(item.resource).then(stat => {
return toDeepIFileStat(provider, [item.resource, stat], item.options && item.options.resolveTo).then(fileStat => {
result[idx] = { stat: fileStat, success: true };
});
}, _err => {
result[idx] = { stat: undefined, success: false };
});
});
return Promise.all(promises).then(() => result);
});
}
// --- resolve
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent> {
if (resource.scheme === Schemas.file) {
return super.resolveContent(resource, options);
} else {
return this._readFile(resource, options).then(RemoteFileService._asContent);
return this._readFile(resource, options).then(LegacyRemoteFileService._asContent);
}
}
@@ -279,7 +96,7 @@ export class RemoteFileService extends FileService {
private _readFile(resource: URI, options: IResolveContentOptions = Object.create(null)): Promise<IStreamContent> {
return this._withProvider(resource).then(provider => {
return this.resolve(resource).then(fileStat => {
return this.fileService.resolve(resource).then(fileStat => {
if (fileStat.isDirectory) {
// todo@joh cannot copy a folder
@@ -334,28 +151,6 @@ export class RemoteFileService extends FileService {
// --- saving
private static async _mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
let basenames: string[] = [];
while (directory.path !== '/') {
try {
let stat = await provider.stat(directory);
if ((stat.type & FileType.Directory) === 0) {
throw new Error(`${directory.toString()} is not a directory`);
}
break; // we have hit a directory -> good
} catch (e) {
// ENOENT
basenames.push(resources.basename(directory));
directory = resources.dirname(directory);
}
}
for (let i = basenames.length - 1; i >= 0; i--) {
directory = resources.joinPath(directory, basenames[i]);
await provider.mkdir(directory);
}
}
private static _throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider {
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED);
@@ -368,9 +163,9 @@ export class RemoteFileService extends FileService {
return super.createFile(resource, content, options);
} else {
return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => {
return this.fileService.createFolder(resources.dirname(resource)).then(() => {
const { encoding } = this.encoding.getWriteEncoding(resource);
return this._writeFile(provider, resource, new StringSnapshot(content || ''), encoding, { create: true, overwrite: Boolean(options && options.overwrite) });
});
@@ -390,8 +185,8 @@ export class RemoteFileService extends FileService {
if (resource.scheme === Schemas.file) {
return super.updateContent(resource, value, options);
} else {
return this._withProvider(resource).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return RemoteFileService._mkdirp(provider, resources.dirname(resource)).then(() => {
return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return this.fileService.createFolder(resources.dirname(resource)).then(() => {
const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value;
return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true });
});
@@ -409,7 +204,7 @@ export class RemoteFileService extends FileService {
target.once('error', err => reject(err));
target.once('finish', (_: unknown) => resolve(undefined));
}).then(_ => {
return this.resolve(resource, { resolveMetadata: true }) as Promise<IFileStatWithMetadata>;
return this.fileService.resolve(resource, { resolveMetadata: true }) as Promise<IFileStatWithMetadata>;
});
}
@@ -430,41 +225,6 @@ export class RemoteFileService extends FileService {
content.value.on('end', () => resolve(result));
});
}
private _activeWatches = new Map<string, { unwatch: Promise<IDisposable>, count: number }>();
watch(resource: URI, opts: IWatchOptions = { recursive: false, excludes: [] }): void {
if (resource.scheme === Schemas.file) {
return super.watch(resource);
}
const key = resource.toString();
const entry = this._activeWatches.get(key);
if (entry) {
entry.count += 1;
return;
}
this._activeWatches.set(key, {
count: 1,
unwatch: this._withProvider(resource).then(provider => {
return provider.watch(resource, opts);
}, _err => {
return { dispose() { } };
})
});
}
unwatch(resource: URI): void {
if (resource.scheme === Schemas.file) {
return super.unwatch(resource);
}
let entry = this._activeWatches.get(resource.toString());
if (entry && --entry.count === 0) {
entry.unwatch.then(dispose);
this._activeWatches.delete(resource.toString());
}
}
}
registerSingleton(ILegacyFileService, RemoteFileService);
registerSingleton(ILegacyFileService, LegacyRemoteFileService);

View File

@@ -1,120 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { Event } from 'vs/base/common/event';
import { IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class FileWatcher {
private static readonly MAX_RESTARTS = 5;
private service: WatcherChannelClient;
private isDisposed: boolean;
private restartCounter: number;
private toDispose: IDisposable[] = [];
constructor(
private contextService: IWorkspaceContextService,
private configurationService: IConfigurationService,
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean,
) {
this.isDisposed = false;
this.restartCounter = 0;
}
public startWatching(): () => void {
const client = new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'File Watcher (nsfw)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
);
this.toDispose.push(client);
client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, null, this.toDispose);
// Initialize watcher
const channel = getNextTickChannel(client.getChannel('watcher'));
this.service = new WatcherChannelClient(channel);
const options = { verboseLogging: this.verboseLogging };
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
onError(err => this.errorLogger(err.message), null, this.toDispose);
const onFileChanges = Event.filter<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose);
// Start watching
this.updateFolders();
this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateFolders()));
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files.watcherExclude')) {
this.updateFolders();
}
}));
return () => this.dispose();
}
private updateFolders() {
if (this.isDisposed) {
return;
}
this.service.setRoots(this.contextService.getWorkspace().folders.filter(folder => {
// Only workspace folders on disk
return folder.uri.scheme === Schemas.file;
}).map(folder => {
// Fetch the root's watcherExclude setting and return it
const configuration = this.configurationService.getValue<IFilesConfiguration>({
resource: folder.uri
});
let ignored: string[] = [];
if (configuration.files && configuration.files.watcherExclude) {
ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]);
}
return {
basePath: folder.uri.fsPath,
ignored
};
}));
}
private dispose(): void {
this.isDisposed = true;
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -1,123 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Schemas } from 'vs/base/common/network';
import { Event } from 'vs/base/common/event';
import { IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class FileWatcher {
private static readonly MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
private service: WatcherChannelClient;
private toDispose: IDisposable[];
constructor(
private contextService: IWorkspaceContextService,
private configurationService: IConfigurationService,
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
this.isDisposed = false;
this.restartCounter = 0;
this.toDispose = [];
}
public startWatching(): () => void {
const args = ['--type=watcherService'];
const client = new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'File Watcher (chokidar)',
args,
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/unix/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
);
this.toDispose.push(client);
client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, null, this.toDispose);
const channel = getNextTickChannel(client.getChannel('watcher'));
this.service = new WatcherChannelClient(channel);
const options = { verboseLogging: this.verboseLogging };
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
onError(err => this.errorLogger(err.message), null, this.toDispose);
const onFileChanges = Event.filter<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose);
// Start watching
this.updateFolders();
this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateFolders()));
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files.watcherExclude')) {
this.updateFolders();
}
}));
return () => this.dispose();
}
private updateFolders() {
if (this.isDisposed) {
return;
}
this.service.setRoots(this.contextService.getWorkspace().folders.filter(folder => {
// Only workspace folders on disk
return folder.uri.scheme === Schemas.file;
}).map(folder => {
// Fetch the root's watcherExclude setting and return it
const configuration = this.configurationService.getValue<IFilesConfiguration>({
resource: folder.uri
});
let ignored: string[] = [];
if (configuration.files && configuration.files.watcherExclude) {
ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]);
}
return {
basePath: folder.uri.fsPath,
ignored,
recursive: false
};
}));
}
private dispose(): void {
this.isDisposed = true;
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -1,70 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRawFileChange, toFileChangesEvent } from 'vs/workbench/services/files/node/watcher/common';
import { OutOfProcessWin32FolderWatcher } from 'vs/workbench/services/files/node/watcher/win32/csharpWatcherService';
import { FileChangesEvent } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { normalize, posix } from 'vs/base/common/path';
import { rtrim, endsWith } from 'vs/base/common/strings';
import { Schemas } from 'vs/base/common/network';
export class FileWatcher {
private isDisposed: boolean;
constructor(
private contextService: IWorkspaceContextService,
private ignored: string[],
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
}
public startWatching(): () => void {
if (this.contextService.getWorkspace().folders[0].uri.scheme !== Schemas.file) {
return () => { };
}
let basePath: string = normalize(this.contextService.getWorkspace().folders[0].uri.fsPath);
if (basePath && basePath.indexOf('\\\\') === 0 && endsWith(basePath, posix.sep)) {
// for some weird reason, node adds a trailing slash to UNC paths
// we never ever want trailing slashes as our base path unless
// someone opens root ("/").
// See also https://github.com/nodejs/io.js/issues/1765
basePath = rtrim(basePath, posix.sep);
}
const watcher = new OutOfProcessWin32FolderWatcher(
basePath,
this.ignored,
events => this.onRawFileEvents(events),
error => this.onError(error),
this.verboseLogging
);
return () => {
this.isDisposed = true;
watcher.dispose();
};
}
private onRawFileEvents(events: IRawFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
}
private onError(error: string): void {
if (!this.isDisposed) {
this.errorLogger(error);
}
}
}

View File

@@ -7,23 +7,26 @@ import * as fs from 'fs';
import * as path from 'vs/base/common/path';
import * as os from 'os';
import * as assert from 'assert';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { FileOperation, FileOperationEvent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { FileOperation, FileOperationEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import { URI as uri } from 'vs/base/common/uri';
import * as uuid from 'vs/base/common/uuid';
import * as pfs from 'vs/base/node/pfs';
import * as encodingLib from 'vs/base/node/encoding';
import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService } from 'vs/workbench/test/workbenchTestServices';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TextModel } from 'vs/editor/common/model/textModel';
import { IEncodingOverride } from 'vs/workbench/services/files/node/encoding';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { NullLogService } from 'vs/platform/log/common/log';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
suite('FileService', () => {
let service: FileService;
suite('LegacyFileService', () => {
let service: LegacyFileService;
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'fileservice');
let testDir: string;
@@ -32,14 +35,22 @@ suite('FileService', () => {
testDir = path.join(parentDir, id);
const sourceDir = getPathFromAmdModule(require, './fixtures/service');
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
return pfs.copy(sourceDir, testDir).then(() => {
service = new FileService(new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true });
service = new LegacyFileService(
fileService,
new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))),
TestEnvironmentService,
new TestTextResourceConfigurationService(),
);
});
});
teardown(() => {
service.dispose();
return pfs.del(parentDir, os.tmpdir());
return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE);
});
test('createFile', () => {
@@ -348,45 +359,6 @@ suite('FileService', () => {
});
});
test('watch', function (done) {
const toWatch = uri.file(path.join(testDir, 'index.html'));
service.watch(toWatch);
service.onFileChanges((e: FileChangesEvent) => {
assert.ok(e);
service.unwatch(toWatch);
done();
});
setTimeout(() => {
fs.writeFileSync(toWatch.fsPath, 'Changes');
}, 100);
});
// test('watch - support atomic save', function (done) {
// const toWatch = uri.file(path.join(testDir, 'index.html'));
// service.watch(toWatch);
// service.onFileChanges((e: FileChangesEvent) => {
// assert.ok(e);
// service.unwatch(toWatch);
// done();
// });
// setTimeout(() => {
// // Simulate atomic save by deleting the file, creating it under different name
// // and then replacing the previously deleted file with those contents
// const renamed = `${toWatch.fsPath}.bak`;
// fs.unlinkSync(toWatch.fsPath);
// fs.writeFileSync(renamed, 'Changes');
// fs.renameSync(renamed, toWatch.fsPath);
// }, 100);
// });
test('options - encoding override (parent)', function () {
// setup
@@ -406,18 +378,16 @@ suite('FileService', () => {
const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService);
const _service = new FileService(
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
const _service = new LegacyFileService(
fileService,
new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))),
TestEnvironmentService,
textResourceConfigurationService,
configurationService,
new TestLifecycleService(),
new TestStorageService(),
new TestNotificationService(),
{
encodingOverride,
disableWatcher: true
});
{ encodingOverride });
return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => {
assert.equal(c.encoding, 'windows1252');
@@ -451,18 +421,15 @@ suite('FileService', () => {
const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService);
const _service = new FileService(
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
const _service = new LegacyFileService(
fileService,
new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))),
TestEnvironmentService,
textResourceConfigurationService,
configurationService,
new TestLifecycleService(),
new TestStorageService(),
new TestNotificationService(),
{
encodingOverride,
disableWatcher: true
});
{ encodingOverride });
return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => {
assert.equal(c.encoding, 'windows1252');
@@ -485,17 +452,15 @@ suite('FileService', () => {
const _sourceDir = getPathFromAmdModule(require, './fixtures/service');
const resource = uri.file(path.join(testDir, 'index.html'));
const _service = new FileService(
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
const _service = new LegacyFileService(
fileService,
new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))),
TestEnvironmentService,
new TestTextResourceConfigurationService(),
new TestConfigurationService(),
new TestLifecycleService(),
new TestStorageService(),
new TestNotificationService(),
{
disableWatcher: true
});
new TestTextResourceConfigurationService()
);
return pfs.copy(_sourceDir, _testDir).then(() => {
return pfs.readFile(resource.fsPath).then(data => {

View File

@@ -1,179 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'vs/base/common/path';
import * as assert from 'assert';
import { StatResolver } from 'vs/workbench/services/files/node/fileService';
import { URI as uri } from 'vs/base/common/uri';
import { isLinux } from 'vs/base/common/platform';
import * as utils from 'vs/workbench/services/files/test/electron-browser/utils';
import { getPathFromAmdModule } from 'vs/base/common/amd';
function create(relativePath: string): StatResolver {
let basePath = getPathFromAmdModule(require, './fixtures/resolver');
let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath;
let fsStat = fs.statSync(absolutePath);
return new StatResolver(uri.file(absolutePath), fsStat.isSymbolicLink(), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, undefined);
}
function toResource(relativePath: string): uri {
let basePath = getPathFromAmdModule(require, './fixtures/resolver');
let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath;
return uri.file(absolutePath);
}
suite('Stat Resolver', () => {
test('resolve file', function () {
let resolver = create('/index.html');
return resolver.resolve(undefined).then(result => {
assert.ok(!result.isDirectory);
assert.equal(result.name, 'index.html');
assert.ok(!!result.etag);
resolver = create('examples');
return resolver.resolve(undefined).then(result => {
assert.ok(result.isDirectory);
});
});
});
test('resolve directory', function () {
let testsElements = ['examples', 'other', 'index.html', 'site.css'];
let resolver = create('/');
return resolver.resolve(undefined).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result!.isDirectory);
assert.equal(result.children!.length, testsElements.length);
assert.ok(result.children!.every((entry) => {
return testsElements.some((name) => {
return path.basename(entry.resource.fsPath) === name;
});
}));
result.children!.forEach((value) => {
assert.ok(path.basename(value.resource.fsPath));
if (['examples', 'other'].indexOf(path.basename(value.resource.fsPath)) >= 0) {
assert.ok(value.isDirectory);
} else if (path.basename(value.resource.fsPath) === 'index.html') {
assert.ok(!value.isDirectory);
assert.ok(!value.children);
} else if (path.basename(value.resource.fsPath) === 'site.css') {
assert.ok(!value.isDirectory);
assert.ok(!value.children);
} else {
assert.ok(!'Unexpected value ' + path.basename(value.resource.fsPath));
}
});
});
});
test('resolve directory - resolveTo single directory', function () {
let resolver = create('/');
return resolver.resolve({ resolveTo: [toResource('other/deep')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children!;
assert.equal(children.length, 4);
const other = utils.getByName(result, 'other');
assert.ok(other);
assert.ok(other!.children!.length > 0);
const deep = utils.getByName(other!, 'deep');
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
});
});
test('resolve directory - resolveTo single directory - mixed casing', function () {
let resolver = create('/');
return resolver.resolve({ resolveTo: [toResource('other/Deep')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children;
assert.equal(children!.length, 4);
const other = utils.getByName(result, 'other');
assert.ok(other);
assert.ok(other!.children!.length > 0);
const deep = utils.getByName(other!, 'deep');
if (isLinux) { // Linux has case sensitive file system
assert.ok(deep);
assert.ok(!deep!.children); // not resolved because we got instructed to resolve other/Deep with capital D
} else {
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
}
});
});
test('resolve directory - resolveTo multiple directories', function () {
let resolver = create('/');
return resolver.resolve({ resolveTo: [toResource('other/deep'), toResource('examples')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children!;
assert.equal(children.length, 4);
const other = utils.getByName(result, 'other');
assert.ok(other);
assert.ok(other!.children!.length > 0);
const deep = utils.getByName(other!, 'deep');
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
const examples = utils.getByName(result, 'examples');
assert.ok(examples);
assert.ok(examples!.children!.length > 0);
assert.equal(examples!.children!.length, 4);
});
});
test('resolve directory - resolveSingleChildFolders', function () {
let resolver = create('/other');
return resolver.resolve({ resolveSingleChildDescendants: true }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children!;
assert.equal(children.length, 1);
let deep = utils.getByName(result, 'deep');
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
});
});
});

View File

@@ -1,20 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IFileStat } from 'vs/platform/files/common/files';
export function getByName(root: IFileStat, name: string): IFileStat | null {
if (root.children === undefined) {
return null;
}
for (const child of root.children) {
if (child.name === name) {
return child;
}
}
return null;
}

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata } from 'vs/platform/files/common/files';
import { Disposable, IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
@@ -14,17 +14,19 @@ import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
import { getBaseLabel } from 'vs/base/common/labels';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer } from 'vs/base/common/buffer';
export class FileService2 extends Disposable implements IFileService {
//#region TODO@Ben HACKS
private _legacy: IFileService | null;
private _legacy: ILegacyFileService | null;
private joinOnLegacy: Promise<ILegacyFileService>;
private joinOnImplResolve: (service: ILegacyFileService) => void;
setLegacyService(legacy: IFileService): void {
setLegacyService(legacy: ILegacyFileService): void {
this._legacy = this._register(legacy);
this._register(legacy.onFileChanges(e => this._onFileChanges.fire(e)));
this._register(legacy.onAfterOperation(e => this._onAfterOperation.fire(e)));
this.provider.forEach((provider, scheme) => {
@@ -38,8 +40,7 @@ export class FileService2 extends Disposable implements IFileService {
_serviceBrand: ServiceIdentifier<any>;
private joinOnLegacy: Promise<IFileService>;
private joinOnImplResolve: (service: IFileService) => void;
private readonly BUFFER_SIZE = 16 * 1024;
constructor(@ILogService private logService: ILogService) {
super();
@@ -75,15 +76,19 @@ export class FileService2 extends Disposable implements IFileService {
this.provider.set(scheme, provider);
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
// Forward change events from provider
const providerFileListener = provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes)));
// Forward events from provider
const providerDisposables: IDisposable[] = [];
providerDisposables.push(provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes))));
if (typeof provider.onDidErrorOccur === 'function') {
providerDisposables.push(provider.onDidErrorOccur(error => this._onError.fire(error)));
}
return combinedDisposable([
toDisposable(() => {
this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider });
this.provider.delete(scheme);
providerFileListener.dispose();
dispose(providerDisposables);
}),
legacyDisposal
]);
@@ -116,10 +121,10 @@ export class FileService2 extends Disposable implements IFileService {
return this.provider.has(resource.scheme);
}
async hasCapability(resource: URI, capability: FileSystemProviderCapabilities): Promise<boolean> {
const provider = await this.withProvider(resource);
hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean {
const provider = this.provider.get(resource.scheme);
return !!(provider.capabilities & capability);
return !!(provider && (provider.capabilities & capability));
}
private async withProvider(resource: URI): Promise<IFileSystemProvider> {
@@ -150,6 +155,9 @@ export class FileService2 extends Disposable implements IFileService {
private _onAfterOperation: Emitter<FileOperationEvent> = this._register(new Emitter<FileOperationEvent>());
get onAfterOperation(): Event<FileOperationEvent> { return this._onAfterOperation.event; }
private _onError: Emitter<Error> = this._register(new Emitter<Error>());
get onError(): Event<Error> { return this._onError.event; }
//#region File Metadata Resolving
async resolve(resource: URI, options: IResolveMetadataFileOptions): Promise<IFileStatWithMetadata>;
@@ -311,12 +319,12 @@ export class FileService2 extends Disposable implements IFileService {
// create file: buffered
if (hasOpenReadWriteCloseCapability(provider)) {
await this.doWriteBuffered(provider, resource, new TextEncoder().encode(content));
await this.doWriteBuffered(provider, resource, VSBuffer.fromString(content || ''));
}
// create file: unbuffered
else if (hasReadWriteCapability(provider)) {
await this.doWriteUnbuffered(provider, resource, new TextEncoder().encode(content), overwrite);
await this.doWriteUnbuffered(provider, resource, VSBuffer.fromString(content || ''), overwrite);
}
// give up if provider has insufficient capabilities
@@ -586,19 +594,73 @@ export class FileService2 extends Disposable implements IFileService {
private _onFileChanges: Emitter<FileChangesEvent> = this._register(new Emitter<FileChangesEvent>());
get onFileChanges(): Event<FileChangesEvent> { return this._onFileChanges.event; }
watch(resource: URI): void {
this.joinOnLegacy.then(legacy => legacy.watch(resource));
private activeWatchers = new Map<string, { disposable: IDisposable, count: number }>();
watch(resource: URI, options: IWatchOptions = { recursive: false, excludes: [] }): IDisposable {
let watchDisposed = false;
let watchDisposable = toDisposable(() => watchDisposed = true);
// Watch and wire in disposable which is async but
// check if we got disposed meanwhile and forward
this.doWatch(resource, options).then(disposable => {
if (watchDisposed) {
dispose(disposable);
} else {
watchDisposable = disposable;
}
}, error => this.logService.error(error));
return toDisposable(() => dispose(watchDisposable));
}
unwatch(resource: URI): void {
this.joinOnLegacy.then(legacy => legacy.unwatch(resource));
async doWatch(resource: URI, options: IWatchOptions): Promise<IDisposable> {
const provider = await this.withProvider(resource);
const key = this.toWatchKey(provider, resource, options);
// Only start watching if we are the first for the given key
const watcher = this.activeWatchers.get(key) || { count: 0, disposable: provider.watch(resource, options) };
if (!this.activeWatchers.has(key)) {
this.activeWatchers.set(key, watcher);
}
// Increment usage counter
watcher.count += 1;
return toDisposable(() => {
// Unref
watcher.count--;
// Dispose only when last user is reached
if (watcher.count === 0) {
dispose(watcher.disposable);
this.activeWatchers.delete(key);
}
});
}
private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string {
const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
return [
isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(), // lowercase path is the provider is case insensitive
String(options.recursive), // use recursive: true | false as part of the key
options.excludes.join() // use excludes as part of the key
].join();
}
dispose(): void {
super.dispose();
this.activeWatchers.forEach(watcher => dispose(watcher.disposable));
this.activeWatchers.clear();
}
//#endregion
//#region Helpers
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: Uint8Array): Promise<void> {
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: VSBuffer): Promise<void> {
// open handle
const handle = await provider.open(resource, { create: true });
@@ -613,16 +675,16 @@ export class FileService2 extends Disposable implements IFileService {
}
}
private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: Uint8Array, length: number, posInFile: number, posInBuffer: number): Promise<void> {
private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: VSBuffer, length: number, posInFile: number, posInBuffer: number): Promise<void> {
let totalBytesWritten = 0;
while (totalBytesWritten < length) {
const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);
const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten);
totalBytesWritten += bytesWritten;
}
}
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, buffer: Uint8Array, overwrite: boolean): Promise<void> {
return provider.writeFile(resource, buffer, { create: true, overwrite });
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, buffer: VSBuffer, overwrite: boolean): Promise<void> {
return provider.writeFile(resource, buffer.buffer, { create: true, overwrite });
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@@ -635,7 +697,7 @@ export class FileService2 extends Disposable implements IFileService {
sourceHandle = await sourceProvider.open(source, { create: false });
targetHandle = await targetProvider.open(target, { create: true });
const buffer = new Uint8Array(16 * 1024);
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
let posInFile = 0;
let posInBuffer = 0;
@@ -643,7 +705,7 @@ export class FileService2 extends Disposable implements IFileService {
do {
// read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at
// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer, posInBuffer, buffer.byteLength - posInBuffer);
bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
// write into target (targetHandle) at current position (posInFile) from buffer (buffer) at
// buffer position (posInBuffer) all bytes we read (bytesRead).
@@ -653,7 +715,7 @@ export class FileService2 extends Disposable implements IFileService {
posInBuffer += bytesRead;
// when buffer full, fill it again from the beginning
if (posInBuffer === buffer.length) {
if (posInBuffer === buffer.byteLength) {
posInBuffer = 0;
}
} while (bytesRead > 0);
@@ -679,7 +741,7 @@ export class FileService2 extends Disposable implements IFileService {
// Read entire buffer from source and write buffered
try {
const buffer = await sourceProvider.readFile(source);
await this.doWriteBuffer(targetProvider, targetHandle, buffer, buffer.byteLength, 0, 0);
await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0);
} catch (error) {
throw error;
} finally {
@@ -689,27 +751,38 @@ export class FileService2 extends Disposable implements IFileService {
private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise<void> {
// Determine file size
const size = (await this.resolve(source, { resolveMetadata: true })).size;
// Open handle
const sourceHandle = await sourceProvider.open(source, { create: false });
try {
const buffer = new Uint8Array(size);
const buffers: VSBuffer[] = [];
let pos = 0;
let buffer = VSBuffer.alloc(this.BUFFER_SIZE);
let posInFile = 0;
let totalBytesRead = 0;
let bytesRead = 0;
let posInBuffer = 0;
do {
// read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at
// read from source (sourceHandle) at current position (pos) into buffer (buffer) at
// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
bytesRead = await sourceProvider.read(sourceHandle, pos, buffer, pos, buffer.byteLength - pos);
bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
pos += bytesRead;
} while (bytesRead > 0 && pos < size);
posInFile += bytesRead;
posInBuffer += bytesRead;
totalBytesRead += bytesRead;
// when buffer full, create a new one
if (posInBuffer === buffer.byteLength) {
buffers.push(buffer);
buffer = VSBuffer.alloc(this.BUFFER_SIZE);
posInBuffer = 0;
}
} while (bytesRead > 0);
// Write buffer into target at once
await this.doWriteUnbuffered(targetProvider, target, buffer, overwrite);
await this.doWriteUnbuffered(targetProvider, target, VSBuffer.concat([...buffers, buffer.slice(0, posInBuffer)], totalBytesRead), overwrite);
} catch (error) {
throw error;
} finally {

View File

@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { ResourceMap } from 'vs/base/common/map';
import { onUnexpectedError } from 'vs/base/common/errors';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
export class WorkspaceWatcher extends Disposable {
private watches = new ResourceMap<IDisposable>();
constructor(
@IFileService private readonly fileService: FileService2,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@INotificationService private readonly notificationService: INotificationService,
@IStorageService private readonly storageService: IStorageService
) {
super();
this.registerListeners();
this.refresh();
}
private registerListeners(): void {
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e)));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));
this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e)));
this._register(this.fileService.onError(error => this.onError(error)));
}
private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void {
// Removed workspace: Unwatch
for (const removed of e.removed) {
this.unwatchWorkspace(removed.uri);
}
// Added workspace: Watch
for (const added of e.added) {
this.watchWorkspace(added.uri);
}
}
private onDidChangeWorkbenchState(): void {
this.refresh();
}
private onDidChangeConfiguration(e: IConfigurationChangeEvent): void {
if (e.affectsConfiguration('files.watcherExclude')) {
this.refresh();
}
}
private onError(error: Error): void {
const msg = error.toString();
// Forward to unexpected error handler
onUnexpectedError(msg);
// Detect if we run < .NET Framework 4.5
if (msg.indexOf('System.MissingMethodException') >= 0 && !this.storageService.getBoolean('ignoreNetVersionError', StorageScope.WORKSPACE)) {
this.notificationService.prompt(
Severity.Warning,
localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."),
[{
label: localize('installNet', "Download .NET Framework 4.5"),
run: () => window.open('https://go.microsoft.com/fwlink/?LinkId=786533')
},
{
label: localize('neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => this.storageService.store('ignoreNetVersionError', true, StorageScope.WORKSPACE)
}],
{ sticky: true }
);
}
// Detect if we run into ENOSPC issues
if (msg.indexOf('ENOSPC') >= 0 && !this.storageService.getBoolean('ignoreEnospcError', StorageScope.WORKSPACE)) {
this.notificationService.prompt(
Severity.Warning,
localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."),
[{
label: localize('learnMore', "Instructions"),
run: () => window.open('https://go.microsoft.com/fwlink/?linkid=867693')
},
{
label: localize('neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => this.storageService.store('ignoreEnospcError', true, StorageScope.WORKSPACE)
}],
{ sticky: true }
);
}
}
private watchWorkspace(resource: URI) {
// Compute the watcher exclude rules from configuration
const excludes: string[] = [];
const config = this.configurationService.getValue<IFilesConfiguration>({ resource });
if (config.files && config.files.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
excludes.push(key);
}
}
}
// Watch workspace
const disposable = this.fileService.watch(resource, { recursive: true, excludes });
this.watches.set(resource, disposable);
}
private unwatchWorkspace(resource: URI) {
if (this.watches.has(resource)) {
dispose(this.watches.get(resource));
this.watches.delete(resource);
}
}
private refresh(): void {
// Unwatch all first
this.unwatchWorkspaces();
// Watch each workspace folder
for (const folder of this.contextService.getWorkspace().folders) {
this.watchWorkspace(folder.uri);
}
}
private unwatchWorkspaces() {
this.watches.forEach(disposable => dispose(disposable));
this.watches.clear();
}
dispose(): void {
super.dispose();
this.unwatchWorkspaces();
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored);

View File

@@ -4,20 +4,24 @@
*--------------------------------------------------------------------------------------------*/
import { mkdir, open, close, read, write } from 'fs';
import { tmpdir } from 'os';
import { promisify } from 'util';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IDisposable, Disposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { statLink, readdir, unlink, del, move, copy, readFile, writeFile, fileExists, truncate } from 'vs/base/node/pfs';
import { statLink, readdir, unlink, move, copy, readFile, writeFile, fileExists, truncate, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { normalize, basename, dirname } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
import { retry } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { retry, ThrottledDelayer } from 'vs/base/common/async';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { localize } from 'vs/nls';
import { IDiskFileChange, toFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher';
import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files2/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files2/node/watcher/win32/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherService';
import { FileWatcher as NodeJSWatcherService } from 'vs/workbench/services/files2/node/watcher/nodejs/watcherService';
export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider {
@@ -65,7 +69,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
ctime: stat.ctime.getTime(),
mtime: stat.mtime.getTime(),
size: stat.size
} as IStat;
};
} catch (error) {
throw this.toFileSystemProviderError(error);
}
@@ -231,7 +235,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (opts.recursive) {
await del(filePath, tmpdir());
await rimraf(filePath, RimRafMode.MOVE);
} else {
await unlink(filePath);
}
@@ -304,11 +308,108 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
//#region File Watching
private _onDidWatchErrorOccur: Emitter<Error> = this._register(new Emitter<Error>());
get onDidErrorOccur(): Event<Error> { return this._onDidWatchErrorOccur.event; }
private _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
get onDidChangeFile(): Event<IFileChange[]> { return this._onDidChangeFile.event; }
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
private recursiveWatchRequestDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(0));
watch(resource: URI, opts: IWatchOptions): IDisposable {
throw new Error('Method not implemented.');
if (opts.recursive) {
return this.watchRecursive(resource, opts.excludes);
}
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
}
private watchRecursive(resource: URI, excludes: string[]): IDisposable {
// Add to list of folders to watch recursively
const folderToWatch = { path: this.toFilePath(resource), excludes };
this.recursiveFoldersToWatch.push(folderToWatch);
// Trigger update
this.refreshRecursiveWatchers();
return toDisposable(() => {
// Remove from list of folders to watch recursively
this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1);
// Trigger update
this.refreshRecursiveWatchers();
});
}
private refreshRecursiveWatchers(): void {
// Buffer requests for recursive watching to decide on right watcher
// that supports potentially watching more than one folder at once
this.recursiveWatchRequestDelayer.trigger(() => {
this.doRefreshRecursiveWatchers();
return Promise.resolve();
});
}
private doRefreshRecursiveWatchers(): void {
// Reuse existing
if (this.recursiveWatcher instanceof NsfwWatcherService) {
this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch);
}
// Create new
else {
// Dispose old
dispose(this.recursiveWatcher);
let watcherImpl: {
new(
folders: { path: string, excludes: string[] }[],
onChange: (changes: IDiskFileChange[]) => void,
onError: (msg: string) => void,
verboseLogging: boolean
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
};
// Single Folder Watcher
if (this.recursiveFoldersToWatch.length === 1) {
if (isWindows) {
watcherImpl = WindowsWatcherService;
} else {
watcherImpl = UnixWatcherService;
}
}
// Multi Folder Watcher
else {
watcherImpl = NsfwWatcherService;
}
// Create and start watching
this.recursiveWatcher = new watcherImpl(
this.recursiveFoldersToWatch,
event => this._onDidChangeFile.fire(toFileChanges(event)),
error => this._onDidWatchErrorOccur.fire(new Error(error)),
this.logService.getLevel() === LogLevel.Trace
);
}
}
private watchNonRecursive(resource: URI): IDisposable {
return new NodeJSWatcherService(
this.toFilePath(resource),
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
error => this._onDidWatchErrorOccur.fire(new Error(error)),
info => this.logService.trace(info),
this.logService.getLevel() === LogLevel.Trace
);
}
//#endregion
@@ -347,4 +448,11 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
}
//#endregion
dispose(): void {
super.dispose();
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
}
}

View File

@@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { statLink, readlink } from 'vs/base/node/pfs';
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { join, basename } from 'vs/base/common/path';
export class FileWatcher extends Disposable {
private isDisposed: boolean;
private fileChangesDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */));
private fileChangesBuffer: IDiskFileChange[] = [];
constructor(
private path: string,
private onFileChanges: (changes: IDiskFileChange[]) => void,
private errorLogger: (msg: string) => void,
private verboseLogger: (msg: string) => void,
private verboseLogging: boolean
) {
super();
this.startWatching();
}
private async startWatching(): Promise<void> {
try {
const { stat, isSymbolicLink } = await statLink(this.path);
if (this.isDisposed) {
return;
}
let pathToWatch = this.path;
if (isSymbolicLink) {
try {
pathToWatch = await readlink(pathToWatch);
} catch (error) {
this.onError(error);
}
}
// Watch Folder
if (stat.isDirectory()) {
this._register(watchFolder(pathToWatch, (eventType, path) => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED,
path: join(this.path, basename(path)) // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
// Watch File
else {
this._register(watchFile(pathToWatch, eventType => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED,
path: this.path // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
} catch (error) {
this.onError(error);
}
}
private onFileChange(event: IDiskFileChange): void {
// Add to buffer
this.fileChangesBuffer.push(event);
// Logging
if (this.verboseLogging) {
this.onVerbose(`[File Watcher (node.js)] ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
// Handle emit through delayer to accommodate for bulk changes and thus reduce spam
this.fileChangesDelayer.trigger(() => {
const fileChanges = this.fileChangesBuffer;
this.fileChangesBuffer = [];
// Event normalization
const normalizedFileChanges = normalizeFileChanges(fileChanges);
// Logging
if (this.verboseLogging) {
normalizedFileChanges.forEach(event => {
this.onVerbose(`[File Watcher (node.js)] >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
});
}
// Fire
if (normalizedFileChanges.length > 0) {
this.onFileChanges(normalizedFileChanges);
}
return Promise.resolve();
});
}
private onError(error: string): void {
if (!this.isDisposed) {
this.errorLogger(error);
}
}
private onVerbose(msg: string): void {
if (!this.isDisposed) {
this.verboseLogger(msg);
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -7,14 +7,14 @@ import * as glob from 'vs/base/common/glob';
import * as extpath from 'vs/base/common/extpath';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import * as watcher from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher';
import * as nsfw from 'vscode-nsfw';
import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { IWatcherService, IWatcherRequest, IWatcherOptions, IWatchError } from 'vs/workbench/services/files2/node/watcher/nsfw/watcher';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
import { normalizeNFC } from 'vs/base/common/normalization';
import { Event, Emitter } from 'vs/base/common/event';
import { realcaseSync, realpathSync } from 'vs/base/node/extfs';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
@@ -39,22 +39,22 @@ export class NsfwWatcherService implements IWatcherService {
private _verboseLogging: boolean;
private enospcErrorLogged: boolean;
private _onWatchEvent = new Emitter<watcher.IRawFileChange[] | IWatchError>();
private _onWatchEvent = new Emitter<IDiskFileChange[] | IWatchError>();
readonly onWatchEvent = this._onWatchEvent.event;
watch(options: IWatcherOptions): Event<watcher.IRawFileChange[] | IWatchError> {
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
this._verboseLogging = options.verboseLogging;
return this.onWatchEvent;
}
private _watch(request: IWatcherRequest): void {
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
this._pathWatchers[request.basePath] = {
this._pathWatchers[request.path] = {
ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
ignored: Array.isArray(request.ignored) ? request.ignored.map(ignored => glob.parse(ignored)) : []
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
};
process.on('uncaughtException', (e: Error | string) => {
@@ -75,30 +75,30 @@ export class NsfwWatcherService implements IWatcherService {
// - the path is a symbolic link
// We have to detect this case and massage the events to correct this.
let realBasePathDiffers = false;
let realBasePathLength = request.basePath.length;
let realBasePathLength = request.path.length;
if (platform.isMacintosh) {
try {
// First check for symbolic link
let realBasePath = realpathSync(request.basePath);
let realBasePath = realpathSync(request.path);
// Second check for casing difference
if (request.basePath === realBasePath) {
realBasePath = (realcaseSync(request.basePath) || request.basePath);
if (request.path === realBasePath) {
realBasePath = (realcaseSync(request.path) || request.path);
}
if (request.basePath !== realBasePath) {
if (request.path !== realBasePath) {
realBasePathLength = realBasePath.length;
realBasePathDiffers = true;
console.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.basePath}, real: ${realBasePath})`);
console.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`);
}
} catch (error) {
// ignore
}
}
nsfw(request.basePath, events => {
nsfw(request.path, events => {
for (const e of events) {
// Logging
if (this._verboseLogging) {
@@ -111,20 +111,20 @@ export class NsfwWatcherService implements IWatcherService {
if (e.action === nsfw.actions.RENAMED) {
// Rename fires when a file's name changes within a single directory
absolutePath = path.join(e.directory, e.oldFile || '');
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
} else if (this._verboseLogging) {
console.log(' >> ignored', absolutePath);
}
absolutePath = path.join(e.directory, e.newFile || '');
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
} else if (this._verboseLogging) {
console.log(' >> ignored', absolutePath);
}
} else {
absolutePath = path.join(e.directory, e.file || '');
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
path: absolutePath
@@ -148,13 +148,13 @@ export class NsfwWatcherService implements IWatcherService {
// Convert paths back to original form in case it differs
if (realBasePathDiffers) {
e.path = request.basePath + e.path.substr(realBasePathLength);
e.path = request.path + e.path.substr(realBasePathLength);
}
});
}
// Broadcast to clients normalized
const res = watcher.normalize(events);
const res = normalizeFileChanges(events);
this._onWatchEvent.fire(res);
// Logging
@@ -167,7 +167,7 @@ export class NsfwWatcherService implements IWatcherService {
return Promise.resolve(undefined);
});
}).then(watcher => {
this._pathWatchers[request.basePath].watcher = watcher;
this._pathWatchers[request.path].watcher = watcher;
const startPromise = watcher.start();
startPromise.then(() => readyPromiseResolve(watcher));
return startPromise;
@@ -180,17 +180,17 @@ export class NsfwWatcherService implements IWatcherService {
// Gather roots that are not currently being watched
const rootsToStartWatching = normalizedRoots.filter(r => {
return !(r.basePath in this._pathWatchers);
return !(r.path in this._pathWatchers);
});
// Gather current roots that don't exist in the new roots array
const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => {
return normalizedRoots.every(normalizedRoot => normalizedRoot.basePath !== r);
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
});
// Logging
if (this._verboseLogging) {
console.log(`Start watching: [${rootsToStartWatching.map(r => r.basePath).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
console.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
// Stop watching some roots
@@ -204,8 +204,8 @@ export class NsfwWatcherService implements IWatcherService {
// Refresh ignored arrays in case they changed
roots.forEach(root => {
if (root.basePath in this._pathWatchers) {
this._pathWatchers[root.basePath].ignored = Array.isArray(root.ignored) ? root.ignored.map(ignored => glob.parse(ignored)) : [];
if (root.path in this._pathWatchers) {
this._pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
}
});
@@ -233,7 +233,7 @@ export class NsfwWatcherService implements IWatcherService {
*/
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
return roots.filter(r => roots.every(other => {
return !(r.basePath.length > other.basePath.length && extpath.isEqualOrParent(r.basePath, other.basePath));
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
}));
}

View File

@@ -6,16 +6,16 @@
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService';
import { IWatcherRequest } from 'vs/workbench/services/files2/node/watcher/nsfw/watcher';
class TestNsfwWatcherService extends NsfwWatcherService {
public normalizeRoots(roots: string[]): string[] {
// Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => {
return { basePath: r, ignored: [] };
return { path: r, excludes: [] };
});
return this._normalizeRoots(requests).map(r => r.basePath);
return this._normalizeRoots(requests).map(r => r.path);
}
}

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
path: string;
excludes: string[];
}
export interface IWatcherOptions {
@@ -20,7 +20,7 @@ export interface IWatchError {
}
export interface IWatcherService {
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError>;
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
stop(): Promise<void>;

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
import { WatcherChannel } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherIpc';
import { NsfwWatcherService } from 'vs/workbench/services/files2/node/watcher/nsfw/nsfwWatcherService';
const server = new Server('watcher');
const service = new NsfwWatcherService();

View File

@@ -6,7 +6,7 @@
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher';
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
export class WatcherChannel implements IServerChannel {
@@ -35,7 +35,7 @@ export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IChannel) { }
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError> {
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
return this.channel.listen('watch', options);
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
import { WatcherChannelClient } from 'vs/workbench/services/files2/node/watcher/nsfw/watcherIpc';
import { Disposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { IWatchError, IWatcherRequest } from 'vs/workbench/services/files2/node/watcher/nsfw/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private service: WatcherChannelClient;
private isDisposed: boolean;
private restartCounter: number;
constructor(
private folders: IWatcherRequest[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean,
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'File Watcher (nsfw)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files2/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[File Watcher (nsfw)] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[File Watcher (nsfw)] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
const channel = getNextTickChannel(client.getChannel('watcher'));
this.service = new WatcherChannelClient(channel);
const options = { verboseLogging: this.verboseLogging };
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
this._register(onError(err => this.errorLogger(`[File Watcher (nsfw)] ${err.message}`)));
const onFileChanges = Event.filter<any, IDiskFileChange[]>(onWatchEvent, (e): e is IDiskFileChange[] => Array.isArray(e) && e.length > 0);
this._register(onFileChanges(e => this.onFileChanges(e)));
// Start watching
this.setFolders(this.folders);
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
this.service.setRoots(folders);
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -12,10 +12,10 @@ import * as glob from 'vs/base/common/glob';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { normalizeNFC } from 'vs/base/common/normalization';
import { realcaseSync } from 'vs/base/node/extfs';
import { realcaseSync } from 'vs/base/node/extpath';
import { isMacintosh } from 'vs/base/common/platform';
import * as watcherCommon from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher';
import { IDiskFileChange, normalizeFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher';
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from 'vs/workbench/services/files2/node/watcher/unix/watcher';
import { Emitter, Event } from 'vs/base/common/event';
interface IWatcher {
@@ -46,10 +46,10 @@ export class ChokidarWatcherService implements IWatcherService {
private spamWarningLogged: boolean;
private enospcErrorLogged: boolean;
private _onWatchEvent = new Emitter<watcherCommon.IRawFileChange[] | IWatchError>();
private _onWatchEvent = new Emitter<IDiskFileChange[] | IWatchError>();
readonly onWatchEvent = this._onWatchEvent.event;
public watch(options: IWatcherOptions & IChockidarWatcherOptions): Event<watcherCommon.IRawFileChange[] | IWatchError> {
public watch(options: IWatcherOptions & IChockidarWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
this._verboseLogging = options.verboseLogging;
this._pollingInterval = options.pollingInterval;
this._watchers = Object.create(null);
@@ -117,7 +117,7 @@ export class ChokidarWatcherService implements IWatcherService {
// if there's only one request, use the built-in ignore-filterering
const isSingleFolder = requests.length === 1;
if (isSingleFolder) {
watcherOpts.ignored = requests[0].ignored;
watcherOpts.ignored = requests[0].excludes;
}
// Chokidar fails when the basePath does not match case-identical to the path on disk
@@ -139,7 +139,7 @@ export class ChokidarWatcherService implements IWatcherService {
console.error('Watcher is not using native fsevents library and is falling back to unefficient polling.');
}
let undeliveredFileEvents: watcherCommon.IRawFileChange[] = [];
let undeliveredFileEvents: IDiskFileChange[] = [];
let fileEventDelayer: ThrottledDelayer<undefined> | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
const watcher: IWatcher = {
@@ -232,7 +232,7 @@ export class ChokidarWatcherService implements IWatcherService {
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = watcherCommon.normalize(events);
const res = normalizeFileChanges(events);
this._onWatchEvent.fire(res);
// Logging
@@ -247,7 +247,7 @@ export class ChokidarWatcherService implements IWatcherService {
}
});
chokidarWatcher.on('error', (error: Error) => {
chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => {
if (error) {
// Specially handle ENOSPC errors that can happen when
@@ -255,7 +255,7 @@ export class ChokidarWatcherService implements IWatcherService {
// we are running into a limit. We only want to warn
// once in this case to avoid log spam.
// See https://github.com/Microsoft/vscode/issues/7950
if ((<any>error).code === 'ENOSPC') {
if (error.code === 'ENOSPC') {
if (!this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this.stop();
@@ -281,19 +281,19 @@ export class ChokidarWatcherService implements IWatcherService {
function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
for (let request of requests) {
if (request.basePath === path) {
if (request.path === path) {
return false;
}
if (extpath.isEqualOrParent(path, request.basePath)) {
if (extpath.isEqualOrParent(path, request.path)) {
if (!request.parsedPattern) {
if (request.ignored && request.ignored.length > 0) {
let pattern = `{${request.ignored.join(',')}}`;
if (request.excludes && request.excludes.length > 0) {
let pattern = `{${request.excludes.join(',')}}`;
request.parsedPattern = glob.parse(pattern);
} else {
request.parsedPattern = () => false;
}
}
const relPath = path.substr(request.basePath.length + 1);
const relPath = path.substr(request.path.length + 1);
if (!request.parsedPattern(relPath)) {
return false;
}
@@ -307,18 +307,18 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
* equests with Sub paths are skipped if they have the same ignored set as the parent.
*/
export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } {
requests = requests.sort((r1, r2) => r1.basePath.localeCompare(r2.basePath));
requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path));
let prevRequest: IWatcherRequest | null = null;
let result: { [basePath: string]: IWatcherRequest[] } = Object.create(null);
for (let request of requests) {
let basePath = request.basePath;
let ignored = (request.ignored || []).sort();
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.basePath))) {
if (!isEqualIgnore(ignored, prevRequest.ignored)) {
result[prevRequest.basePath].push({ basePath, ignored });
let basePath = request.path;
let ignored = (request.excludes || []).sort();
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) {
if (!isEqualIgnore(ignored, prevRequest.excludes)) {
result[prevRequest.path].push({ path: basePath, excludes: ignored });
}
} else {
prevRequest = { basePath, ignored };
prevRequest = { path: basePath, excludes: ignored };
result[basePath] = [prevRequest];
}
}
@@ -330,7 +330,7 @@ function isEqualRequests(r1: IWatcherRequest[], r2: IWatcherRequest[]) {
return false;
}
for (let k = 0; k < r1.length; k++) {
if (r1[k].basePath !== r2[k].basePath || !isEqualIgnore(r1[k].ignored, r2[k].ignored)) {
if (r1[k].path !== r2[k].path || !isEqualIgnore(r1[k].excludes, r2[k].excludes)) {
return false;
}
}

View File

@@ -7,17 +7,15 @@ import * as assert from 'assert';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { normalizeRoots, ChokidarWatcherService } from '../chokidarWatcherService';
import { IWatcherRequest } from '../watcher';
import * as platform from 'vs/base/common/platform';
import { Delayer } from 'vs/base/common/async';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
return { basePath, ignored };
return { path: basePath, excludes: ignored };
}
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
@@ -32,13 +30,13 @@ function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequ
const expectedPaths = Object.keys(expectedRequests).sort();
assert.deepEqual(actualPath, expectedPaths);
for (let path of actualPath) {
let a = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath));
let e = expectedRequests[path].sort((r1, r2) => r1.basePath.localeCompare(r2.basePath));
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
assert.deepEqual(a, e);
}
}
function sort(changes: IRawFileChange[]) {
function sort(changes: IDiskFileChange[]) {
return changes.sort((c1, c2) => {
return c1.path.localeCompare(c2.path);
});
@@ -48,7 +46,7 @@ function wait(time: number) {
return new Delayer<void>(time).trigger(() => { });
}
async function assertFileEvents(actuals: IRawFileChange[], expected: IRawFileChange[]) {
async function assertFileEvents(actuals: IDiskFileChange[], expected: IDiskFileChange[]) {
let repeats = 40;
while ((actuals.length < expected.length) && repeats-- > 0) {
await wait(50);
@@ -126,7 +124,7 @@ suite.skip('Chockidar watching', () => {
const b2Folder = path.join(bFolder, 'b2');
const service = new ChokidarWatcherService();
const result: IRawFileChange[] = [];
const result: IDiskFileChange[] = [];
let error: string | null = null;
suiteSetup(async () => {
@@ -147,7 +145,7 @@ suite.skip('Chockidar watching', () => {
});
suiteTeardown(async () => {
await pfs.del(testDir);
await pfs.rimraf(testDir, pfs.RimRafMode.MOVE);
await service.stop();
});
@@ -161,7 +159,7 @@ suite.skip('Chockidar watching', () => {
});
test('simple file operations, single root, no ignore', async () => {
let request: IWatcherRequest = { basePath: testDir, ignored: [] };
let request: IWatcherRequest = { path: testDir, excludes: [] };
service.setRoots([request]);
await wait(300);
@@ -185,19 +183,19 @@ suite.skip('Chockidar watching', () => {
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.ADDED }, { path: testFolderPath, type: FileChangeType.ADDED }]);
// delete a file
await pfs.del(copiedFilePath);
await pfs.rimraf(copiedFilePath, pfs.RimRafMode.MOVE);
let renamedFilePath = path.join(testFolderPath, 'file3.txt');
// move a file
await pfs.rename(testFilePath, renamedFilePath);
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.DELETED }, { path: testFilePath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.ADDED }]);
// delete a folder
await pfs.del(testFolderPath);
await pfs.rimraf(testFolderPath, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]);
});
test('simple file operations, ignore', async () => {
let request: IWatcherRequest = { basePath: testDir, ignored: ['**/b/**', '**/*.js', '.git/**'] };
let request: IWatcherRequest = { path: testDir, excludes: ['**/b/**', '**/*.js', '.git/**'] };
service.setRoots([request]);
await wait(300);
@@ -234,21 +232,21 @@ suite.skip('Chockidar watching', () => {
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.ADDED }, { path: movedFile3, type: FileChangeType.ADDED }]);
// delete all files
await pfs.del(movedFile1); // hidden
await pfs.del(movedFile2);
await pfs.del(movedFile3);
await pfs.del(folder1); // hidden
await pfs.del(folder2); // hidden
await pfs.del(folder3); // hidden
await pfs.del(folder4);
await pfs.del(folder5);
await pfs.del(file4);
await pfs.rimraf(movedFile1); // hidden
await pfs.rimraf(movedFile2, pfs.RimRafMode.MOVE);
await pfs.rimraf(movedFile3, pfs.RimRafMode.MOVE);
await pfs.rimraf(folder1); // hidden
await pfs.rimraf(folder2); // hidden
await pfs.rimraf(folder3); // hidden
await pfs.rimraf(folder4, pfs.RimRafMode.MOVE);
await pfs.rimraf(folder5, pfs.RimRafMode.MOVE);
await pfs.rimraf(file4, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.DELETED }, { path: movedFile3, type: FileChangeType.DELETED }, { path: file4, type: FileChangeType.DELETED }, { path: folder4, type: FileChangeType.DELETED }, { path: folder5, type: FileChangeType.DELETED }]);
});
test('simple file operations, multiple roots', async () => {
let request1: IWatcherRequest = { basePath: aFolder, ignored: ['**/*.js'] };
let request2: IWatcherRequest = { basePath: b2Folder, ignored: ['**/*.ts'] };
let request1: IWatcherRequest = { path: aFolder, excludes: ['**/*.js'] };
let request2: IWatcherRequest = { path: b2Folder, excludes: ['**/*.ts'] };
service.setRoots([request1, request2]);
await wait(300);
@@ -271,23 +269,23 @@ suite.skip('Chockidar watching', () => {
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.ADDED }, { path: filePath1, type: FileChangeType.ADDED }, { path: folderPath2, type: FileChangeType.ADDED }]);
// change roots
let request3: IWatcherRequest = { basePath: aFolder, ignored: ['**/*.json'] };
let request3: IWatcherRequest = { path: aFolder, excludes: ['**/*.json'] };
service.setRoots([request3]);
await wait(300);
assert.equal(service.wacherCount, 1);
// delete all
await pfs.del(folderPath1);
await pfs.del(folderPath2);
await pfs.del(filePath4);
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
await pfs.rimraf(folderPath2, pfs.RimRafMode.MOVE);
await pfs.rimraf(filePath4, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]);
});
test('simple file operations, nested roots', async () => {
let request1: IWatcherRequest = { basePath: testDir, ignored: ['**/b2/**'] };
let request2: IWatcherRequest = { basePath: bFolder, ignored: ['**/b3/**'] };
let request1: IWatcherRequest = { path: testDir, excludes: ['**/b2/**'] };
let request2: IWatcherRequest = { path: bFolder, excludes: ['**/b3/**'] };
service.setRoots([request1, request2]);
await wait(300);
@@ -311,8 +309,8 @@ suite.skip('Chockidar watching', () => {
await assertFileEvents(result, [{ path: filePath2, type: FileChangeType.DELETED }]);
// delete all
await pfs.del(folderPath1);
await pfs.del(filePath1);
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
await pfs.rimraf(filePath1, pfs.RimRafMode.MOVE);
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]);
});

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
path: string;
excludes: string[];
}
export interface IWatcherOptions {
@@ -20,7 +20,7 @@ export interface IWatchError {
}
export interface IWatcherService {
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError>;
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
stop(): Promise<void>;

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
import { ChokidarWatcherService } from 'vs/workbench/services/files/node/watcher/unix/chokidarWatcherService';
import { WatcherChannel } from 'vs/workbench/services/files2/node/watcher/unix/watcherIpc';
import { ChokidarWatcherService } from 'vs/workbench/services/files2/node/watcher/unix/chokidarWatcherService';
const server = new Server('watcher');
const service = new ChokidarWatcherService();

View File

@@ -6,7 +6,7 @@
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService, IWatcherOptions, IWatchError } from './watcher';
import { Event } from 'vs/base/common/event';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
export class WatcherChannel implements IServerChannel {
@@ -35,7 +35,7 @@ export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IChannel) { }
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError> {
watch(options: IWatcherOptions): Event<IDiskFileChange[] | IWatchError> {
return this.channel.listen('watch', options);
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
import { WatcherChannelClient } from 'vs/workbench/services/files2/node/watcher/unix/watcherIpc';
import { Disposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { IWatchError, IWatcherRequest } from 'vs/workbench/services/files2/node/watcher/unix/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
private service: WatcherChannelClient;
constructor(
private folders: IWatcherRequest[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'File Watcher (chokidar)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files2/node/watcher/unix/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[File Watcher (chokidar)] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[File Watcher (chokidar)] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
const channel = getNextTickChannel(client.getChannel('watcher'));
this.service = new WatcherChannelClient(channel);
const options = { verboseLogging: this.verboseLogging };
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
this._register(onError(err => this.errorLogger(`[File Watcher (chokidar)] ${err.message}`)));
const onFileChanges = Event.filter<any, IDiskFileChange[]>(onWatchEvent, (e): e is IDiskFileChange[] => Array.isArray(e) && e.length > 0);
this._register(onFileChanges(e => this.onFileChanges(e)));
// Start watching
this.service.setRoots(this.folders);
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
this.service.setRoots(folders);
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -4,32 +4,25 @@
*--------------------------------------------------------------------------------------------*/
import { URI as uri } from 'vs/base/common/uri';
import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files';
import { FileChangeType, isParent, IFileChange } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
export interface IRawFileChange {
export interface IDiskFileChange {
type: FileChangeType;
path: string;
}
export function toFileChangesEvent(changes: IRawFileChange[]): FileChangesEvent {
// map to file changes event that talks about URIs
return new FileChangesEvent(changes.map((c) => {
return {
type: c.type,
resource: uri.file(c.path)
};
export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] {
return changes.map(change => ({
type: change.type,
resource: uri.file(change.path)
}));
}
/**
* Given events that occurred, applies some rules to normalize the events
*/
export function normalize(changes: IRawFileChange[]): IRawFileChange[] {
export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] {
// Build deltas
let normalizer = new EventNormalizer();
const normalizer = new EventNormalizer();
for (const event of changes) {
normalizer.processEvent(event);
}
@@ -38,25 +31,20 @@ export function normalize(changes: IRawFileChange[]): IRawFileChange[] {
}
class EventNormalizer {
private normalized: IRawFileChange[];
private mapPathToChange: { [path: string]: IRawFileChange };
private normalized: IDiskFileChange[] = [];
private mapPathToChange: Map<string, IDiskFileChange> = new Map();
constructor() {
this.normalized = [];
this.mapPathToChange = Object.create(null);
}
public processEvent(event: IRawFileChange): void {
processEvent(event: IDiskFileChange): void {
const existingEvent = this.mapPathToChange.get(event.path);
// Event path already exists
let existingEvent = this.mapPathToChange[event.path];
if (existingEvent) {
let currentChangeType = existingEvent.type;
let newChangeType = event.type;
const currentChangeType = existingEvent.type;
const newChangeType = event.type;
// ignore CREATE followed by DELETE in one go
if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) {
delete this.mapPathToChange[event.path];
this.mapPathToChange.delete(event.path);
this.normalized.splice(this.normalized.indexOf(existingEvent), 1);
}
@@ -66,8 +54,7 @@ class EventNormalizer {
}
// Do nothing. Keep the created event
else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) {
}
else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { }
// Otherwise apply change type
else {
@@ -75,16 +62,16 @@ class EventNormalizer {
}
}
// Otherwise Store
// Otherwise store new
else {
this.normalized.push(event);
this.mapPathToChange[event.path] = event;
this.mapPathToChange.set(event.path, event);
}
}
public normalize(): IRawFileChange[] {
let addedChangeEvents: IRawFileChange[] = [];
let deletedPaths: string[] = [];
normalize(): IDiskFileChange[] {
const addedChangeEvents: IDiskFileChange[] = [];
const deletedPaths: string[] = [];
// This algorithm will remove all DELETE events up to the root folder
// that got deleted if any. This ensures that we are not producing
@@ -96,6 +83,7 @@ class EventNormalizer {
return this.normalized.filter(e => {
if (e.type !== FileChangeType.DELETED) {
addedChangeEvents.push(e);
return false; // remove ADD / CHANGE
}

View File

@@ -4,12 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import { FileChangeType } from 'vs/platform/files/common/files';
import * as decoder from 'vs/base/node/decoder';
import * as glob from 'vs/base/common/glob';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export class OutOfProcessWin32FolderWatcher {
@@ -26,7 +24,7 @@ export class OutOfProcessWin32FolderWatcher {
constructor(
private watchedFolder: string,
ignored: string[],
private eventCallback: (events: IRawFileChange[]) => void,
private eventCallback: (events: IDiskFileChange[]) => void,
private errorCallback: (error: string) => void,
private verboseLogging: boolean
) {
@@ -38,6 +36,11 @@ export class OutOfProcessWin32FolderWatcher {
this.ignored = [];
}
// Logging
if (this.verboseLogging) {
console.log('%c[File Watcher (C#)]', 'color: blue', `Start watching: ${watchedFolder}`);
}
this.startWatcher();
}
@@ -47,7 +50,7 @@ export class OutOfProcessWin32FolderWatcher {
args.push('-verbose');
}
this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/workbench/services/files/node/watcher/win32/CodeHelper.exe'), args);
this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/workbench/services/files2/node/watcher/win32/CodeHelper.exe'), args);
const stdoutLineDecoder = new decoder.LineDecoder();
@@ -55,7 +58,7 @@ export class OutOfProcessWin32FolderWatcher {
this.handle.stdout.on('data', (data: Buffer) => {
// Collect raw events from output
const rawEvents: IRawFileChange[] = [];
const rawEvents: IDiskFileChange[] = [];
stdoutLineDecoder.write(data).forEach((line) => {
const eventParts = line.split('|');
if (eventParts.length === 2) {

View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange } from 'vs/workbench/services/files2/node/watcher/watcher';
import { OutOfProcessWin32FolderWatcher } from 'vs/workbench/services/files2/node/watcher/win32/csharpWatcherService';
import { posix } from 'vs/base/common/path';
import { rtrim, endsWith } from 'vs/base/common/strings';
import { Disposable } from 'vs/base/common/lifecycle';
export class FileWatcher extends Disposable {
private isDisposed: boolean;
private folder: { path: string, excludes: string[] };
constructor(
folders: { path: string, excludes: string[] }[],
private onFileChanges: (changes: IDiskFileChange[]) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
super();
this.folder = folders[0];
if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) {
// for some weird reason, node adds a trailing slash to UNC paths
// we never ever want trailing slashes as our base path unless
// someone opens root ("/").
// See also https://github.com/nodejs/io.js/issues/1765
this.folder.path = rtrim(this.folder.path, posix.sep);
}
this.startWatching();
}
private startWatching(): void {
this._register(new OutOfProcessWin32FolderWatcher(
this.folder.path,
this.folder.excludes,
events => this.onFileEvents(events),
error => this.onError(error),
this.verboseLogging
));
}
private onFileEvents(events: IDiskFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(events);
}
}
private onError(error: string): void {
if (!this.isDisposed) {
this.errorLogger(error);
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -7,9 +7,10 @@ import * as assert from 'assert';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices';
import { NullLogService } from 'vs/platform/log/common/log';
import { timeout } from 'vs/base/common/async';
suite('File Service 2', () => {
@@ -50,8 +51,8 @@ suite('File Service 2', () => {
await service.activateProvider('test');
assert.equal(callCount, 2); // activation is called again
assert.equal(await service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
assert.equal(await service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
registrationDisposable!.dispose();
@@ -61,4 +62,51 @@ suite('File Service 2', () => {
assert.equal(registrations[1].scheme, 'test');
assert.equal(registrations[1].added, false);
});
test('watch', async () => {
const service = new FileService2(new NullLogService());
let disposeCounter = 0;
service.registerProvider('test', new NullFileSystemProvider(() => {
return toDisposable(() => {
disposeCounter++;
});
}));
await service.activateProvider('test');
const resource1 = URI.parse('test://foo/bar1');
const watcher1Disposable = service.watch(resource1);
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
watcher1Disposable.dispose();
assert.equal(disposeCounter, 1);
disposeCounter = 0;
const resource2 = URI.parse('test://foo/bar2');
const watcher2Disposable1 = service.watch(resource2);
const watcher2Disposable2 = service.watch(resource2);
const watcher2Disposable3 = service.watch(resource2);
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
watcher2Disposable1.dispose();
assert.equal(disposeCounter, 0);
watcher2Disposable2.dispose();
assert.equal(disposeCounter, 0);
watcher2Disposable3.dispose();
assert.equal(disposeCounter, 1);
disposeCounter = 0;
const resource3 = URI.parse('test://foo/bar3');
const watcher3Disposable1 = service.watch(resource3);
const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] });
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
watcher3Disposable1.dispose();
assert.equal(disposeCounter, 1);
watcher3Disposable2.dispose();
assert.equal(disposeCounter, 2);
});
});

View File

@@ -12,15 +12,14 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { generateUuid } from 'vs/base/common/uuid';
import { join, basename, dirname, posix } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { copy, del, symlink } from 'vs/base/node/pfs';
import { copy, rimraf, symlink, RimRafMode, rimrafSync } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { existsSync, statSync, readdirSync, readFileSync } from 'fs';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'fs';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { promisify } from 'util';
import { exec } from 'child_process';
import { isEqual } from 'vs/base/common/resources';
function getByName(root: IFileStat, name: string): IFileStat | null {
if (root.children === undefined) {
@@ -78,10 +77,12 @@ suite('Disk File Service', () => {
disposables.push(service);
fileProvider = new TestDiskFileSystemProvider(logService);
service.registerProvider(Schemas.file, fileProvider);
disposables.push(service.registerProvider(Schemas.file, fileProvider));
disposables.push(fileProvider);
testProvider = new TestDiskFileSystemProvider(logService);
service.registerProvider(testSchema, testProvider);
disposables.push(service.registerProvider(testSchema, testProvider));
disposables.push(testProvider);
const id = generateUuid();
testDir = join(parentDir, id);
@@ -93,7 +94,7 @@ suite('Disk File Service', () => {
teardown(async () => {
disposables = dispose(disposables);
await del(parentDir, tmpdir());
await rimraf(parentDir, RimRafMode.MOVE);
});
test('createFolder', async () => {
@@ -775,4 +776,230 @@ suite('Disk File Service', () => {
assert.ok(error);
}
});
});
test('watch - file', done => {
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
writeFileSync(toWatch.fsPath, 'Init');
assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50);
});
test('watch - file symbolic link', async done => {
if (isWindows) {
return done(); // not happy
}
const toWatch = URI.file(join(testDir, 'lorem.txt-linked'));
await symlink(join(testDir, 'lorem.txt'), toWatch.fsPath);
assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes'), 50);
});
test('watch - file - multiple writes', done => {
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
writeFileSync(toWatch.fsPath, 'Init');
assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 1'), 0);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 2'), 10);
setTimeout(() => writeFileSync(toWatch.fsPath, 'Changes 3'), 20);
});
test('watch - file - delete file', done => {
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
writeFileSync(toWatch.fsPath, 'Init');
assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done);
setTimeout(() => unlinkSync(toWatch.fsPath), 50);
});
test('watch - file - rename file', done => {
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
const toWatchRenamed = URI.file(join(testDir, 'index-watch1-renamed.html'));
writeFileSync(toWatch.fsPath, 'Init');
assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done);
setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50);
});
test('watch - file - rename file (different case)', done => {
const toWatch = URI.file(join(testDir, 'index-watch1.html'));
const toWatchRenamed = URI.file(join(testDir, 'INDEX-watch1.html'));
writeFileSync(toWatch.fsPath, 'Init');
if (isLinux) {
assertWatch(toWatch, [[FileChangeType.DELETED, toWatch]], done);
} else {
assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done); // case insensitive file system treat this as change
}
setTimeout(() => renameSync(toWatch.fsPath, toWatchRenamed.fsPath), 50);
});
test('watch - file (atomic save)', function (done) {
const toWatch = URI.file(join(testDir, 'index-watch2.html'));
writeFileSync(toWatch.fsPath, 'Init');
assertWatch(toWatch, [[FileChangeType.UPDATED, toWatch]], done);
setTimeout(() => {
// Simulate atomic save by deleting the file, creating it under different name
// and then replacing the previously deleted file with those contents
const renamed = `${toWatch.fsPath}.bak`;
unlinkSync(toWatch.fsPath);
writeFileSync(renamed, 'Changes');
renameSync(renamed, toWatch.fsPath);
}, 50);
});
test('watch - folder (non recursive) - change file', done => {
const watchDir = URI.file(join(testDir, 'watch3'));
mkdirSync(watchDir.fsPath);
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');
assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done);
setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50);
});
test('watch - folder (non recursive) - add file', done => {
const watchDir = URI.file(join(testDir, 'watch4'));
mkdirSync(watchDir.fsPath);
const file = URI.file(join(watchDir.fsPath, 'index.html'));
assertWatch(watchDir, [[FileChangeType.ADDED, file]], done);
setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50);
});
test('watch - folder (non recursive) - delete file', done => {
const watchDir = URI.file(join(testDir, 'watch5'));
mkdirSync(watchDir.fsPath);
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');
assertWatch(watchDir, [[FileChangeType.DELETED, file]], done);
setTimeout(() => unlinkSync(file.fsPath), 50);
});
test('watch - folder (non recursive) - add folder', done => {
const watchDir = URI.file(join(testDir, 'watch6'));
mkdirSync(watchDir.fsPath);
const folder = URI.file(join(watchDir.fsPath, 'folder'));
assertWatch(watchDir, [[FileChangeType.ADDED, folder]], done);
setTimeout(() => mkdirSync(folder.fsPath), 50);
});
test('watch - folder (non recursive) - delete folder', done => {
const watchDir = URI.file(join(testDir, 'watch7'));
mkdirSync(watchDir.fsPath);
const folder = URI.file(join(watchDir.fsPath, 'folder'));
mkdirSync(folder.fsPath);
assertWatch(watchDir, [[FileChangeType.DELETED, folder]], done);
setTimeout(() => rimrafSync(folder.fsPath), 50);
});
test('watch - folder (non recursive) - symbolic link - change file', async done => {
if (isWindows) {
return done(); // not happy
}
const watchDir = URI.file(join(testDir, 'deep-link'));
await symlink(join(testDir, 'deep'), watchDir.fsPath);
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');
assertWatch(watchDir, [[FileChangeType.UPDATED, file]], done);
setTimeout(() => writeFileSync(file.fsPath, 'Changes'), 50);
});
test('watch - folder (non recursive) - rename file', done => {
const watchDir = URI.file(join(testDir, 'watch8'));
mkdirSync(watchDir.fsPath);
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');
const fileRenamed = URI.file(join(watchDir.fsPath, 'index-renamed.html'));
assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done);
setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50);
});
test('watch - folder (non recursive) - rename file (different case)', done => {
if (!isLinux) {
return done(); // not happy
}
const watchDir = URI.file(join(testDir, 'watch8'));
mkdirSync(watchDir.fsPath);
const file = URI.file(join(watchDir.fsPath, 'index.html'));
writeFileSync(file.fsPath, 'Init');
const fileRenamed = URI.file(join(watchDir.fsPath, 'INDEX.html'));
assertWatch(watchDir, [[FileChangeType.DELETED, file], [FileChangeType.ADDED, fileRenamed]], done);
setTimeout(() => renameSync(file.fsPath, fileRenamed.fsPath), 50);
});
function assertWatch(toWatch: URI, expected: [FileChangeType, URI][], done: MochaDone): void {
const watcherDisposable = service.watch(toWatch);
function toString(type: FileChangeType): string {
switch (type) {
case FileChangeType.ADDED: return 'added';
case FileChangeType.DELETED: return 'deleted';
case FileChangeType.UPDATED: return 'updated';
}
}
const listenerDisposable = service.onFileChanges(event => {
watcherDisposable.dispose();
listenerDisposable.dispose();
try {
assert.equal(event.changes.length, expected.length);
if (expected.length === 1) {
assert.equal(event.changes[0].type, expected[0][0], `Expected ${toString(expected[0][0])} but got ${toString(event.changes[0].type)}`);
assert.equal(event.changes[0].resource.fsPath, expected[0][1].fsPath);
} else {
for (const expect of expected) {
assert.equal(hasChange(event.changes, expect[0], expect[1]), true, `Unable to find ${toString(expect[0])} for ${expect[1].fsPath}`);
}
}
done();
} catch (error) {
done(error);
}
});
}
function hasChange(changes: IFileChange[], type: FileChangeType, resource: URI): boolean {
return changes.some(change => change.type === type && isEqual(change.resource, resource));
}
});

View File

@@ -4,13 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
import { URI as uri } from 'vs/base/common/uri';
import { IRawFileChange, toFileChangesEvent, normalize } from 'vs/workbench/services/files/node/watcher/common';
import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/workbench/services/files2/node/watcher/watcher';
import { Event, Emitter } from 'vs/base/common/event';
function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes));
}
class TestFileWatcher {
private readonly _onFileChanges: Emitter<FileChangesEvent>;
@@ -18,18 +21,18 @@ class TestFileWatcher {
this._onFileChanges = new Emitter<FileChangesEvent>();
}
public get onFileChanges(): Event<FileChangesEvent> {
get onFileChanges(): Event<FileChangesEvent> {
return this._onFileChanges.event;
}
public report(changes: IRawFileChange[]): void {
report(changes: IDiskFileChange[]): void {
this.onRawFileEvents(changes);
}
private onRawFileEvents(events: IRawFileChange[]): void {
private onRawFileEvents(events: IDiskFileChange[]): void {
// Normalize
let normalizedEvents = normalize(events);
let normalizedEvents = normalizeFileChanges(events);
// Emit through event emitter
if (normalizedEvents.length > 0) {
@@ -44,16 +47,16 @@ enum Path {
UNC
}
suite('Watcher', () => {
suite('Normalizer', () => {
test('watching - simple add/update/delete', function (done: () => void) {
test('simple add/update/delete', function (done: () => void) {
const watch = new TestFileWatcher();
const added = uri.file('/users/data/src/added.txt');
const updated = uri.file('/users/data/src/updated.txt');
const deleted = uri.file('/users/data/src/deleted.txt');
const raw: IRawFileChange[] = [
const raw: IDiskFileChange[] = [
{ path: added.fsPath, type: FileChangeType.ADDED },
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: deleted.fsPath, type: FileChangeType.DELETED },
@@ -74,7 +77,7 @@ suite('Watcher', () => {
let pathSpecs = platform.isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX];
pathSpecs.forEach((p) => {
test('watching - delete only reported for top level folder (' + p + ')', function (done: () => void) {
test('delete only reported for top level folder (' + p + ')', function (done: () => void) {
const watch = new TestFileWatcher();
const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1');
@@ -87,7 +90,7 @@ suite('Watcher', () => {
const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const raw: IRawFileChange[] = [
const raw: IDiskFileChange[] = [
{ path: deletedFolderA.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderB.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF1.fsPath, type: FileChangeType.DELETED },
@@ -115,14 +118,14 @@ suite('Watcher', () => {
});
});
test('watching - event normalization: ignore CREATE followed by DELETE', function (done: () => void) {
test('event normalization: ignore CREATE followed by DELETE', function (done: () => void) {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
const deleted = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
const raw: IDiskFileChange[] = [
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: deleted.fsPath, type: FileChangeType.DELETED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
@@ -140,14 +143,14 @@ suite('Watcher', () => {
watch.report(raw);
});
test('watching - event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) {
test('event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) {
const watch = new TestFileWatcher();
const deleted = uri.file('/users/data/src/related');
const created = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
const raw: IDiskFileChange[] = [
{ path: deleted.fsPath, type: FileChangeType.DELETED },
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
@@ -166,14 +169,14 @@ suite('Watcher', () => {
watch.report(raw);
});
test('watching - event normalization: ignore UPDATE when CREATE received', function (done: () => void) {
test('event normalization: ignore UPDATE when CREATE received', function (done: () => void) {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
const updated = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
const raw: IDiskFileChange[] = [
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
@@ -193,7 +196,7 @@ suite('Watcher', () => {
watch.report(raw);
});
test('watching - event normalization: apply DELETE', function (done: () => void) {
test('event normalization: apply DELETE', function (done: () => void) {
const watch = new TestFileWatcher();
const updated = uri.file('/users/data/src/related');
@@ -201,7 +204,7 @@ suite('Watcher', () => {
const deleted = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
const raw: IDiskFileChange[] = [
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: updated2.fsPath, type: FileChangeType.UPDATED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },

View File

@@ -1,32 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export const IHashService = createDecorator<IHashService>('hashService');
export interface IHashService {
_serviceBrand: any;
/**
* Produce a SHA1 hash of the provided content.
*/
createSHA1(content: string): Thenable<string>;
}
export class HashService implements IHashService {
_serviceBrand: any;
createSHA1(content: string): Thenable<string> {
return crypto.subtle.digest('SHA-1', new TextEncoder().encode(content)).then(buffer => {
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Converting_a_digest_to_a_hex_string
return Array.prototype.map.call(new Uint8Array(buffer), (value: number) => `00${value.toString(16)}`.slice(-2)).join('');
});
}
}
registerSingleton(IHashService, HashService, true);

View File

@@ -1,19 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createHash } from 'crypto';
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class HashService implements IHashService {
_serviceBrand: any;
createSHA1(content: string): Promise<string> {
return Promise.resolve(createHash('sha1').update(content).digest('hex'));
}
}
registerSingleton(IHashService, HashService, true);

View File

@@ -1,20 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { HashService } from 'vs/workbench/services/hash/common/hashService';
suite('Hash Service', () => {
test('computeSHA1Hash', async () => {
const service = new HashService();
assert.equal(await service.createSHA1(''), 'da39a3ee5e6b4b0d3255bfef95601890afd80709');
assert.equal(await service.createSHA1('hello world'), '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed');
assert.equal(await service.createSHA1('da39a3ee5e6b4b0d3255bfef95601890afd80709'), '10a34637ad661d98ba3344717656fcc76209c2f8');
assert.equal(await service.createSHA1('2aae6c35c94fcfb415dbe95f408b9ce91ee846ed'), 'd6b0d82cea4269b51572b8fab43adcee9fc3cf9a');
assert.equal(await service.createSHA1('öäü_?ß()<>ÖÄÜ'), 'b64beaeff9e317b0193c8e40a2431b210388eba9');
});
});

View File

@@ -24,7 +24,7 @@ import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search';
import { IExpression } from 'vs/base/common/glob';
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { ResourceGlobMatcher } from 'vs/workbench/common/resources';
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
@@ -98,7 +98,7 @@ interface IRecentlyClosedFile {
export class HistoryService extends Disposable implements IHistoryService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<any>;
private static readonly STORAGE_KEY = 'history.entries';
private static readonly MAX_HISTORY_ITEMS = 200;
@@ -838,7 +838,7 @@ export class HistoryService extends Disposable implements IHistoryService {
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map(input => {
const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map((input): ISerializedEditorHistoryEntry | undefined => {
// Editor input: try via factory
if (input instanceof EditorInput) {
@@ -846,14 +846,14 @@ export class HistoryService extends Disposable implements IHistoryService {
if (factory) {
const deserialized = factory.serialize(input);
if (deserialized) {
return { editorInputJSON: { typeId: input.getTypeId(), deserialized } } as ISerializedEditorHistoryEntry;
return { editorInputJSON: { typeId: input.getTypeId(), deserialized } };
}
}
}
// File resource: via URI.toJSON()
else {
return { resourceJSON: (input as IResourceInput).resource.toJSON() } as ISerializedEditorHistoryEntry;
return { resourceJSON: (input as IResourceInput).resource.toJSON() };
}
return undefined;
@@ -884,11 +884,11 @@ export class HistoryService extends Disposable implements IHistoryService {
}
private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined {
const serializedEditorHistoryEntry = entry as ISerializedEditorHistoryEntry;
const serializedEditorHistoryEntry = entry;
// File resource: via URI.revive()
if (serializedEditorHistoryEntry.resourceJSON) {
return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) } as IResourceInput;
return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) };
}
// Editor input: via factory

View File

@@ -11,8 +11,7 @@ import * as json from 'vs/base/common/json';
import { ChordKeybinding, KeyCode, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { OS } from 'vs/base/common/platform';
import * as uuid from 'vs/base/common/uuid';
import * as extfs from 'vs/base/node/extfs';
import { mkdirp } from 'vs/base/node/pfs';
import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { IModelService } from 'vs/editor/common/services/modelService';
@@ -21,7 +20,6 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
@@ -31,20 +29,22 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IWorkspaceContextService, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestLogService, TestStorageService, TestTextFileService, TestTextResourceConfigurationService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices';
import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestLogService, TestTextFileService, TestTextResourceConfigurationService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
interface Modifiers {
metaKey?: boolean;
@@ -82,16 +82,15 @@ suite('KeybindingsEditing', () => {
instantiationService.stub(ILogService, new TestLogService());
instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService)));
instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl));
instantiationService.stub(IFileService, new FileService(
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
fileService.setLegacyService(new LegacyFileService(
fileService,
new TestContextService(new Workspace(testDir, toWorkspaceFolders([{ path: testDir }]))),
TestEnvironmentService,
new TestTextResourceConfigurationService(),
new TestConfigurationService(),
lifecycleService,
new TestStorageService(),
new TestNotificationService(),
{ disableWatcher: true })
);
));
instantiationService.stub(IFileService, fileService);
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
@@ -101,15 +100,15 @@ suite('KeybindingsEditing', () => {
});
});
async function setUpWorkspace(): Promise<boolean> {
async function setUpWorkspace(): Promise<void> {
testDir = path.join(os.tmpdir(), 'vsctests', uuid.generateUuid());
return await mkdirp(testDir, 493);
}
teardown(() => {
return new Promise<void>((c, e) => {
return new Promise<void>((c) => {
if (testDir) {
extfs.del(testDir, os.tmpdir(), () => c(undefined), () => c(undefined));
rimraf(testDir, RimRafMode.MOVE).then(c, c);
} else {
c(undefined);
}

View File

@@ -182,13 +182,15 @@ export class LabelService implements ILabelService {
}
// Workspace: Saved
const filename = basename(workspace.configPath);
const workspaceName = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
let filename = basename(workspace.configPath);
if (endsWith(filename, WORKSPACE_EXTENSION)) {
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
}
let label;
if (options && options.verbose) {
label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspace.configPath), workspaceName)));
label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspace.configPath), filename)));
} else {
label = localize('workspaceName', "{0} (Workspace)", workspaceName);
label = localize('workspaceName', "{0} (Workspace)", filename);
}
return this.appendWorkspaceSuffix(label, workspace.configPath);
}

View File

@@ -17,7 +17,7 @@ import { isNumber } from 'vs/base/common/types';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { binarySearch } from 'vs/base/common/arrays';
import { toUint8ArrayBuffer } from 'vs/base/common/uint';
import { VSBuffer } from 'vs/base/common/buffer';
export interface IOutputChannelModel extends IDisposable {
readonly onDidAppendedContent: Event<void>;
@@ -129,6 +129,7 @@ export abstract class AbstractFileOutputChannelModel extends Disposable implemen
}
}
// TODO@ben see if new watchers can cope with spdlog and avoid polling then
class OutputFileListener extends Disposable {
private readonly _onDidContentChange = new Emitter<number | undefined>();
@@ -259,10 +260,7 @@ class FileOutputChannelModel extends AbstractFileOutputChannelModel implements I
}
protected getByteLength(str: string): number {
if (typeof Buffer !== 'undefined') {
return Buffer.from(str).byteLength;
}
return toUint8ArrayBuffer(str).byteLength;
return VSBuffer.fromString(str).byteLength;
}
update(size?: number): void {

View File

@@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as extfs from 'vs/base/node/extfs';
import { dirname, join } from 'vs/base/common/path';
import { join } from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { ITextModel } from 'vs/editor/common/model';
import { URI } from 'vs/base/common/uri';
@@ -13,7 +12,7 @@ import { ThrottledDelayer } from 'vs/base/common/async';
import { IFileService } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IOutputChannelModel, AbstractFileOutputChannelModel, IOutputChannelModelService, AsbtractOutputChannelModelService, BufferredOutputChannel } from 'vs/workbench/services/output/common/outputChannelModel';
import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender';
@@ -24,34 +23,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
let watchingOutputDir = false;
let callbacks: ((eventType: string, fileName?: string) => void)[] = [];
function watchOutputDirectory(outputDir: string, logService: ILogService, onChange: (eventType: string, fileName: string) => void): IDisposable {
callbacks.push(onChange);
if (!watchingOutputDir) {
const watcherDisposable = extfs.watch(outputDir, (eventType, fileName) => {
for (const callback of callbacks) {
callback(eventType, fileName);
}
}, (error: string) => {
logService.error(error);
});
watchingOutputDir = true;
return toDisposable(() => {
callbacks = [];
watcherDisposable.dispose();
});
}
return toDisposable(() => { });
}
class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implements IOutputChannelModel {
private appender: OutputAppender;
private appendedMessage: string;
private loadingFromFileInProgress: boolean;
private resettingDelayer: ThrottledDelayer<void>;
private readonly rotatingFilePath: string;
private readonly rotatingFilePath: URI;
constructor(
id: string,
@@ -69,8 +47,16 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement
// Use one rotating file to check for main file reset
this.appender = new OutputAppender(id, this.file.fsPath);
this.rotatingFilePath = `${id}.1.log`;
this._register(watchOutputDirectory(dirname(this.file.fsPath), logService, (eventType, file) => this.onFileChangedInOutputDirector(eventType, file)));
const rotatingFilePathDirectory = resources.dirname(this.file);
this.rotatingFilePath = resources.joinPath(rotatingFilePathDirectory, `${id}.1.log`);
this._register(fileService.watch(rotatingFilePathDirectory));
this._register(fileService.onFileChanges(e => {
if (e.contains(this.rotatingFilePath)) {
this.resettingDelayer.trigger(() => this.resetModel());
}
}));
this.resettingDelayer = new ThrottledDelayer<void>(50);
}
@@ -143,13 +129,6 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implement
}
}
private onFileChangedInOutputDirector(eventType: string, fileName?: string): void {
// Check if rotating file has changed. It changes only when the main file exceeds its limit.
if (this.rotatingFilePath === fileName) {
this.resettingDelayer.trigger(() => this.resetModel());
}
}
private write(content: string): void {
this.appender.append(content);
}

View File

@@ -38,7 +38,7 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput {
return DefaultPreferencesEditorInput.ID;
}
matches(other: any): boolean {
matches(other: unknown): boolean {
if (other instanceof DefaultPreferencesEditorInput) {
return true;
}
@@ -49,11 +49,19 @@ export class DefaultPreferencesEditorInput extends ResourceEditorInput {
}
}
export interface IKeybindingsEditorSearchOptions {
searchValue: string;
recordKeybindings: boolean;
sortByPrecedence: boolean;
}
export class KeybindingsEditorInput extends EditorInput {
static readonly ID: string = 'workbench.input.keybindings';
readonly keybindingsModel: KeybindingsEditorModel;
searchOptions: IKeybindingsEditorSearchOptions | null;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
super();
this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS);
@@ -71,7 +79,7 @@ export class KeybindingsEditorInput extends EditorInput {
return Promise.resolve(this.keybindingsModel);
}
matches(otherInput: any): boolean {
matches(otherInput: unknown): boolean {
return otherInput instanceof KeybindingsEditorInput;
}
}
@@ -93,7 +101,7 @@ export class SettingsEditor2Input extends EditorInput {
this._settingsModel = _preferencesService.createSettings2EditorModel();
}
matches(otherInput: any): boolean {
matches(otherInput: unknown): boolean {
return otherInput instanceof SettingsEditor2Input;
}

View File

@@ -31,7 +31,7 @@ export class ProgressService2 implements IProgressService2 {
@IStatusbarService private readonly _statusbarService: IStatusbarService,
) { }
withProgress<R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: () => void): Promise<R> {
withProgress<R = unknown>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: () => void): Promise<R> {
const { location } = options;
if (typeof location === 'string') {
@@ -58,7 +58,7 @@ export class ProgressService2 implements IProgressService2 {
}
}
private _withWindowProgress<R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => Promise<R>): Promise<R> {
private _withWindowProgress<R = unknown>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => Promise<R>): Promise<R> {
const task: [IProgressOptions, Progress<IProgressStep>] = [options, new Progress<IProgressStep>(() => this._updateWindowProgress())];
@@ -126,7 +126,7 @@ export class ProgressService2 implements IProgressService2 {
}
}
private _withNotificationProgress<P extends Promise<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P {
private _withNotificationProgress<P extends Promise<R>, R = unknown>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P {
const toDispose: IDisposable[] = [];
const createNotification = (message: string | undefined, increment?: number): INotificationHandle | undefined => {
@@ -221,7 +221,7 @@ export class ProgressService2 implements IProgressService2 {
return p;
}
private _withViewletProgress<P extends Promise<R>, R=any>(viewletId: string, task: (progress: IProgress<{ message?: string }>) => P): P {
private _withViewletProgress<P extends Promise<R>, R = unknown>(viewletId: string, task: (progress: IProgress<{ message?: string }>) => P): P {
const promise = task(emptyProgress);

View File

@@ -53,7 +53,7 @@ export interface ISearchResultProvider {
clearCache(cacheKey: string): Promise<void>;
}
export interface IFolderQuery<U extends UriComponents=URI> {
export interface IFolderQuery<U extends UriComponents = URI> {
folder: U;
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;

View File

@@ -0,0 +1,413 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { IProgress } from 'vs/platform/progress/common/progress';
export class Position {
constructor(readonly line: number, readonly character: number) { }
isBefore(other: Position): boolean { return false; }
isBeforeOrEqual(other: Position): boolean { return false; }
isAfter(other: Position): boolean { return false; }
isAfterOrEqual(other: Position): boolean { return false; }
isEqual(other: Position): boolean { return false; }
compareTo(other: Position): number { return 0; }
translate(lineDelta?: number, characterDelta?: number): Position;
translate(change: { lineDelta?: number; characterDelta?: number; }): Position;
translate(_?: any, _2?: any): Position { return new Position(0, 0); }
with(line?: number, character?: number): Position;
with(change: { line?: number; character?: number; }): Position;
with(_: any): Position { return new Position(0, 0); }
}
export class Range {
readonly start: Position;
readonly end: Position;
constructor(startLine: number, startCol: number, endLine: number, endCol: number) {
this.start = new Position(startLine, startCol);
this.end = new Position(endLine, endCol);
}
isEmpty: boolean;
isSingleLine: boolean;
contains(positionOrRange: Position | Range): boolean { return false; }
isEqual(other: Range): boolean { return false; }
intersection(range: Range): Range | undefined { return undefined; }
union(other: Range): Range { return new Range(0, 0, 0, 0); }
with(start?: Position, end?: Position): Range;
with(change: { start?: Position, end?: Position }): Range;
with(_: any): Range { return new Range(0, 0, 0, 0); }
}
export type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null>;
/**
* A relative pattern is a helper to construct glob patterns that are matched
* relatively to a base path. The base path can either be an absolute file path
* or a [workspace folder](#WorkspaceFolder).
*/
export interface RelativePattern {
/**
* A base file path to which this pattern will be matched against relatively.
*/
base: string;
/**
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
* relative to the base path.
*
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
* the file glob pattern will match on `index.js`.
*/
pattern: string;
}
/**
* A file glob pattern to match file paths against. This can either be a glob pattern string
* (like `**/*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern).
*
* Glob patterns can have the following syntax:
* * `*` to match one or more characters in a path segment
* * `?` to match on one character in a path segment
* * `**` to match any number of path segments, including none
* * `{}` to group conditions (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
*
* Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file
* path to match against, consider to use the [relative pattern](#RelativePattern) support
* that takes care of converting any backslash into slash. Otherwise, make sure to convert
* any backslash to slash when creating the glob pattern.
*/
export type GlobPattern = string | RelativePattern;
/**
* The parameters of a query for text search.
*/
export interface TextSearchQuery {
/**
* The text pattern to search for.
*/
pattern: string;
/**
* Whether or not `pattern` should match multiple lines of text.
*/
isMultiline?: boolean;
/**
* Whether or not `pattern` should be interpreted as a regular expression.
*/
isRegExp?: boolean;
/**
* Whether or not the search should be case-sensitive.
*/
isCaseSensitive?: boolean;
/**
* Whether or not to search for whole word matches only.
*/
isWordMatch?: boolean;
}
/**
* A file glob pattern to match file paths against.
* TODO@roblou - merge this with the GlobPattern docs/definition in vscode.d.ts.
* @see [GlobPattern](#GlobPattern)
*/
export type GlobString = string;
/**
* Options common to file and text search
*/
export interface SearchOptions {
/**
* The root folder to search within.
*/
folder: URI;
/**
* Files that match an `includes` glob pattern should be included in the search.
*/
includes: GlobString[];
/**
* Files that match an `excludes` glob pattern should be excluded from the search.
*/
excludes: GlobString[];
/**
* Whether external files that exclude files, like .gitignore, should be respected.
* See the vscode setting `"search.useIgnoreFiles"`.
*/
useIgnoreFiles: boolean;
/**
* Whether symlinks should be followed while searching.
* See the vscode setting `"search.followSymlinks"`.
*/
followSymlinks: boolean;
/**
* Whether global files that exclude files, like .gitignore, should be respected.
* See the vscode setting `"search.useGlobalIgnoreFiles"`.
*/
useGlobalIgnoreFiles: boolean;
}
/**
* Options to specify the size of the result text preview.
* These options don't affect the size of the match itself, just the amount of preview text.
*/
export interface TextSearchPreviewOptions {
/**
* The maximum number of lines in the preview.
* Only search providers that support multiline search will ever return more than one line in the match.
*/
matchLines: number;
/**
* The maximum number of characters included per line.
*/
charsPerLine: number;
}
/**
* Options that apply to text search.
*/
export interface TextSearchOptions extends SearchOptions {
/**
* The maximum number of results to be returned.
*/
maxResults: number;
/**
* Options to specify the size of the result text preview.
*/
previewOptions?: TextSearchPreviewOptions;
/**
* Exclude files larger than `maxFileSize` in bytes.
*/
maxFileSize?: number;
/**
* Interpret files using this encoding.
* See the vscode setting `"files.encoding"`
*/
encoding?: string;
/**
* Number of lines of context to include before each match.
*/
beforeContext?: number;
/**
* Number of lines of context to include after each match.
*/
afterContext?: number;
}
/**
* Information collected when text search is complete.
*/
export interface TextSearchComplete {
/**
* Whether the search hit the limit on the maximum number of search results.
* `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results.
* - If exactly that number of matches exist, this should be false.
* - If `maxResults` matches are returned and more exist, this should be true.
* - If search hits an internal limit which is less than `maxResults`, this should be true.
*/
limitHit?: boolean;
}
/**
* The parameters of a query for file search.
*/
export interface FileSearchQuery {
/**
* The search pattern to match against file paths.
*/
pattern: string;
}
/**
* Options that apply to file search.
*/
export interface FileSearchOptions extends SearchOptions {
/**
* The maximum number of results to be returned.
*/
maxResults?: number;
/**
* A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache,
* and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared.
*/
session?: CancellationToken;
}
/**
* A preview of the text result.
*/
export interface TextSearchMatchPreview {
/**
* The matching lines of text, or a portion of the matching line that contains the match.
*/
text: string;
/**
* The Range within `text` corresponding to the text of the match.
* The number of matches must match the TextSearchMatch's range property.
*/
matches: Range | Range[];
}
/**
* A match from a text search
*/
export interface TextSearchMatch {
/**
* The uri for the matching document.
*/
uri: URI;
/**
* The range of the match within the document, or multiple ranges for multiple matches.
*/
ranges: Range | Range[];
/**
* A preview of the text match.
*/
preview: TextSearchMatchPreview;
}
/**
* A line of context surrounding a TextSearchMatch.
*/
export interface TextSearchContext {
/**
* The uri for the matching document.
*/
uri: URI;
/**
* One line of text.
* previewOptions.charsPerLine applies to this
*/
text: string;
/**
* The line number of this line of context.
*/
lineNumber: number;
}
export type TextSearchResult = TextSearchMatch | TextSearchContext;
/**
* A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions.
*
* A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for
* all files that match the user's query.
*
* The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string,
* and in that case, every file in the folder should be returned.
*/
export interface FileSearchProvider {
/**
* Provide the set of files that match a certain file path pattern.
* @param query The parameters for this query.
* @param options A set of options to consider while searching files.
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult<URI[]>;
}
/**
* A TextSearchProvider provides search results for text results inside files in the workspace.
*/
export interface TextSearchProvider {
/**
* Provide results that match the given text pattern.
* @param query The parameters for this query.
* @param options A set of options to consider while searching.
* @param progress A progress callback that must be invoked for all results.
* @param token A cancellation token.
*/
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: IProgress<TextSearchResult>, token: CancellationToken): ProviderResult<TextSearchComplete>;
}
/**
* Options that can be set on a findTextInFiles search.
*/
export interface FindTextInFilesOptions {
/**
* A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern
* will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern)
* to restrict the search results to a [workspace folder](#WorkspaceFolder).
*/
include?: GlobPattern;
/**
* A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern
* will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will
* apply, when `null` no excludes will apply.
*/
exclude?: GlobPattern | null;
/**
* The maximum number of results to search for
*/
maxResults?: number;
/**
* Whether external files that exclude files, like .gitignore, should be respected.
* See the vscode setting `"search.useIgnoreFiles"`.
*/
useIgnoreFiles?: boolean;
/**
* Whether global files that exclude files, like .gitignore, should be respected.
* See the vscode setting `"search.useGlobalIgnoreFiles"`.
*/
useGlobalIgnoreFiles?: boolean;
/**
* Whether symlinks should be followed while searching.
* See the vscode setting `"search.followSymlinks"`.
*/
followSymlinks?: boolean;
/**
* Interpret files using this encoding.
* See the vscode setting `"files.encoding"`
*/
encoding?: string;
/**
* Options to specify the size of the result text preview.
*/
previewOptions?: TextSearchPreviewOptions;
/**
* Number of lines of context to include before each match.
*/
beforeContext?: number;
/**
* Number of lines of context to include after each match.
*/
afterContext?: number;
}

View File

@@ -19,8 +19,7 @@ import { StopWatch } from 'vs/base/common/stopwatch';
import * as strings from 'vs/base/common/strings';
import * as types from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import * as extfs from 'vs/base/node/extfs';
import * as flow from 'vs/base/node/flow';
import { readdir } from 'vs/base/node/pfs';
import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search';
import { spawnRipgrepCmd } from './ripgrepFileSearch';
@@ -128,7 +127,7 @@ export class FileWalker {
this.cmdSW = StopWatch.create(false);
// For each root folder
flow.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => {
this.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => {
this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => {
if (err) {
const errorMessage = toErrorMessage(err);
@@ -146,6 +145,34 @@ export class FileWalker {
});
}
private parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
const results = new Array(list.length);
const errors = new Array<Error | null>(list.length);
let didErrorOccur = false;
let doneCount = 0;
if (list.length === 0) {
return callback(null, []);
}
list.forEach((item, index) => {
fn(item, (error, result) => {
if (error) {
didErrorOccur = true;
results[index] = null;
errors[index] = error;
} else {
results[index] = result;
errors[index] = null;
}
if (++doneCount === list.length) {
return callback(didErrorOccur ? errors : null, results);
}
});
});
}
private call<F extends Function>(fun: F, that: any, ...args: any[]): void {
try {
fun.apply(that, args);
@@ -440,7 +467,7 @@ export class FileWalker {
// Execute tasks on each file in parallel to optimize throughput
const hasSibling = glob.hasSiblingFn(() => files);
flow.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => {
this.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => {
// Check canceled
if (this.isCanceled || this.isLimitHit) {
@@ -489,12 +516,14 @@ export class FileWalker {
this.walkedPaths[realpath] = true; // remember as walked
// Continue walking
return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => {
if (error || this.isCanceled || this.isLimitHit) {
return readdir(currentAbsolutePath).then(children => {
if (this.isCanceled || this.isLimitHit) {
return clb(null);
}
this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err || null));
}, error => {
clb(null);
});
});
}

View File

@@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources';
import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import * as vscode from 'vscode';
import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes';
export interface IInternalFileMatch {
base: URI;
@@ -45,7 +45,7 @@ class FileSearchEngine {
private globalExcludePattern?: glob.ParsedExpression;
constructor(private config: IFileQuery, private provider: vscode.FileSearchProvider, private sessionToken?: CancellationToken) {
constructor(private config: IFileQuery, private provider: FileSearchProvider, private sessionToken?: CancellationToken) {
this.filePattern = config.filePattern;
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || undefined;
@@ -172,7 +172,7 @@ class FileSearchEngine {
});
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.FileSearchOptions {
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): FileSearchOptions {
const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern);
@@ -283,7 +283,7 @@ export class FileSearchManager {
private readonly sessions = new Map<string, CancellationTokenSource>();
fileSearch(config: IFileQuery, provider: vscode.FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
fileSearch(config: IFileQuery, provider: FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const sessionTokenSource = this.getSessionTokenSource(config.cacheKey);
const engine = new FileSearchEngine(config, provider, sessionTokenSource && sessionTokenSource.token);

View File

@@ -144,7 +144,8 @@ function globExprsToRgGlobs(patterns: glob.IExpression, folder?: string, exclude
}
globArgs.push(fixDriveC(key));
} else if (value && value.when) {
// {{SQL CARBON EDIT}} @todo anthonydresser cast value because we aren't using strict null checks
} else if (value && (<glob.SiblingClause>value).when) {
siblingClauses[key] = value;
}
});

View File

@@ -3,24 +3,25 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils';
import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
import * as vscode from 'vscode';
import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes';
import { Progress } from 'vs/platform/progress/common/progress';
export class RipgrepSearchProvider implements vscode.TextSearchProvider {
private inProgress: Set<vscode.CancellationTokenSource> = new Set();
export class RipgrepSearchProvider implements TextSearchProvider {
private inProgress: Set<CancellationTokenSource> = new Set();
constructor(private outputChannel: OutputChannel) {
process.once('exit', () => this.dispose());
}
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress<TextSearchResult>, token: CancellationToken): Promise<TextSearchComplete> {
const engine = new RipgrepTextSearchEngine(this.outputChannel);
return this.withToken(token, token => engine.provideTextSearchResults(query, options, progress, token));
}
private async withToken<T>(token: vscode.CancellationToken, fn: (token: vscode.CancellationToken) => Promise<T>): Promise<T> {
private async withToken<T>(token: CancellationToken, fn: (token: CancellationToken) => Promise<T>): Promise<T> {
const merged = mergedTokenSource(token);
this.inProgress.add(merged);
const result = await fn(merged.token);
@@ -34,7 +35,7 @@ export class RipgrepSearchProvider implements vscode.TextSearchProvider {
}
}
function mergedTokenSource(token: vscode.CancellationToken): vscode.CancellationTokenSource {
function mergedTokenSource(token: CancellationToken): CancellationTokenSource {
const tokenSource = new CancellationTokenSource();
token.onCancellationRequested(() => tokenSource.cancel());

View File

@@ -6,8 +6,9 @@
import { startsWith } from 'vs/base/common/strings';
import { ILogService } from 'vs/platform/log/common/log';
import { SearchRange, TextSearchMatch } from 'vs/workbench/services/search/common/search';
import * as vscode from 'vscode';
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { URI } from 'vs/base/common/uri';
import * as searchExtTypes from 'vs/workbench/services/search/common/searchExtTypes';
export type Maybe<T> = T | null | undefined;
@@ -16,9 +17,9 @@ export function anchorGlob(glob: string): string {
}
/**
* Create a vscode.TextSearchResult by using our internal TextSearchResult type for its previewOptions logic.
* Create a vscode.TextSearchMatch by using our internal TextSearchMatch type for its previewOptions logic.
*/
export function createTextSearchResult(uri: vscode.Uri, text: string, range: Range | Range[], previewOptions?: vscode.TextSearchPreviewOptions): vscode.TextSearchMatch {
export function createTextSearchResult(uri: URI, text: string, range: searchExtTypes.Range | searchExtTypes.Range[], previewOptions?: searchExtTypes.TextSearchPreviewOptions): searchExtTypes.TextSearchMatch {
const searchRange = mapArrayOrNot(range, rangeToSearchRange);
const internalResult = new TextSearchMatch(text, searchRange, previewOptions);
@@ -33,50 +34,12 @@ export function createTextSearchResult(uri: vscode.Uri, text: string, range: Ran
};
}
function rangeToSearchRange(range: Range): SearchRange {
function rangeToSearchRange(range: searchExtTypes.Range): SearchRange {
return new SearchRange(range.start.line, range.start.character, range.end.line, range.end.character);
}
function searchRangeToRange(range: SearchRange): Range {
return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
}
export class Position {
constructor(readonly line: number, readonly character: number) { }
isBefore(other: Position): boolean { return false; }
isBeforeOrEqual(other: Position): boolean { return false; }
isAfter(other: Position): boolean { return false; }
isAfterOrEqual(other: Position): boolean { return false; }
isEqual(other: Position): boolean { return false; }
compareTo(other: Position): number { return 0; }
translate(lineDelta?: number, characterDelta?: number): Position;
translate(change: { lineDelta?: number; characterDelta?: number; }): Position;
translate(_?: any, _2?: any): Position { return new Position(0, 0); }
with(line?: number, character?: number): Position;
with(change: { line?: number; character?: number; }): Position;
with(_: any): Position { return new Position(0, 0); }
}
export class Range {
readonly start: Position;
readonly end: Position;
constructor(startLine: number, startCol: number, endLine: number, endCol: number) {
this.start = new Position(startLine, startCol);
this.end = new Position(endLine, endCol);
}
isEmpty: boolean;
isSingleLine: boolean;
contains(positionOrRange: Position | Range): boolean { return false; }
isEqual(other: Range): boolean { return false; }
intersection(range: Range): Range | undefined { return undefined; }
union(other: Range): Range { return new Range(0, 0, 0, 0); }
with(start?: Position, end?: Position): Range;
with(change: { start?: Position, end?: Position }): Range;
with(_: any): Range { return new Range(0, 0, 0, 0); }
function searchRangeToRange(range: SearchRange): searchExtTypes.Range {
return new searchExtTypes.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
}
export interface IOutputChannel {

View File

@@ -10,12 +10,14 @@ import { NodeStringDecoder, StringDecoder } from 'string_decoder';
import { createRegExp, startsWith, startsWithUTF8BOM, stripUTF8BOM, escapeRegExpCharacters, endsWith } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IExtendedExtensionSearchOptions, SearchError, SearchErrorCode, serializeSearchError } from 'vs/workbench/services/search/common/search';
import * as vscode from 'vscode';
import { rgPath } from 'vscode-ripgrep';
import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe, Range } from './ripgrepSearchUtils';
import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe } from './ripgrepSearchUtils';
import { coalesce } from 'vs/base/common/arrays';
import { splitGlobAware } from 'vs/base/common/glob';
import { groupBy } from 'vs/base/common/collections';
import { TextSearchQuery, TextSearchOptions, TextSearchResult, TextSearchComplete, TextSearchPreviewOptions, TextSearchContext, TextSearchMatch, Range } from 'vs/workbench/services/search/common/searchExtTypes';
import { Progress } from 'vs/platform/progress/common/progress';
import { CancellationToken } from 'vs/base/common/cancellation';
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
@@ -24,7 +26,7 @@ export class RipgrepTextSearchEngine {
constructor(private outputChannel: IOutputChannel) { }
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress<TextSearchResult>, token: CancellationToken): Promise<TextSearchComplete> {
this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({
...options,
...{
@@ -53,7 +55,7 @@ export class RipgrepTextSearchEngine {
let gotResult = false;
const ripgrepParser = new RipgrepParser(options.maxResults, cwd, options.previewOptions);
ripgrepParser.on('result', (match: vscode.TextSearchResult) => {
ripgrepParser.on('result', (match: TextSearchResult) => {
gotResult = true;
progress.report(match);
});
@@ -155,7 +157,7 @@ export class RipgrepParser extends EventEmitter {
private numResults = 0;
constructor(private maxResults: number, private rootFolder: string, private previewOptions?: vscode.TextSearchPreviewOptions) {
constructor(private maxResults: number, private rootFolder: string, private previewOptions?: TextSearchPreviewOptions) {
super();
this.stringDecoder = new StringDecoder();
}
@@ -169,7 +171,7 @@ export class RipgrepParser extends EventEmitter {
}
on(event: 'result', listener: (result: vscode.TextSearchResult) => void): this;
on(event: 'result', listener: (result: TextSearchResult) => void): this;
on(event: 'hitLimit', listener: () => void): this;
on(event: string, listener: (...args: any[]) => void): this {
super.on(event, listener);
@@ -240,7 +242,7 @@ export class RipgrepParser extends EventEmitter {
}
}
private createTextSearchMatch(data: IRgMatch, uri: vscode.Uri): vscode.TextSearchMatch {
private createTextSearchMatch(data: IRgMatch, uri: URI): TextSearchMatch {
const lineNumber = data.line_number - 1;
let isBOMStripped = false;
let fullText = bytesOrTextToString(data.lines);
@@ -290,7 +292,7 @@ export class RipgrepParser extends EventEmitter {
return createTextSearchResult(uri, fullText, <Range[]>ranges, this.previewOptions);
}
private createTextSearchContext(data: IRgMatch, uri: URI): vscode.TextSearchContext[] {
private createTextSearchContext(data: IRgMatch, uri: URI): TextSearchContext[] {
const text = bytesOrTextToString(data.lines);
const startLine = data.line_number;
return text
@@ -305,7 +307,7 @@ export class RipgrepParser extends EventEmitter {
});
}
private onResult(match: vscode.TextSearchResult): void {
private onResult(match: TextSearchResult): void {
this.emit('result', match);
}
}
@@ -333,7 +335,7 @@ function getNumLinesAndLastNewlineLength(text: string): { numLines: number, last
return { numLines, lastLineLength };
}
function getRgArgs(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions): string[] {
function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] {
const args = ['--hidden'];
args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case');

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import * as extfs from 'vs/base/node/extfs';
import * as pfs from 'vs/base/node/pfs';
import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchStats, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search';
import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
@@ -30,7 +30,7 @@ export class TextSearchEngineAdapter {
onMessage({ message: msg });
}
};
const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), extfs);
const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs);
return new Promise((resolve, reject) => {
return textSearchManager
.search(

View File

@@ -11,9 +11,9 @@ import * as glob from 'vs/base/common/glob';
import * as resources from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { toCanonicalName } from 'vs/base/node/encoding';
import * as extfs from 'vs/base/node/extfs';
import * as pfs from 'vs/base/node/pfs';
import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import * as vscode from 'vscode';
import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes';
export class TextSearchManager {
@@ -22,7 +22,7 @@ export class TextSearchManager {
private isLimitHit: boolean;
private resultCount = 0;
constructor(private query: ITextQuery, private provider: vscode.TextSearchProvider, private _extfs: typeof extfs = extfs) {
constructor(private query: ITextQuery, private provider: TextSearchProvider, private _pfs: typeof pfs = pfs) {
}
search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
@@ -34,7 +34,7 @@ export class TextSearchManager {
this.collector = new TextSearchResultsCollector(onProgress);
let isCanceled = false;
const onResult = (result: vscode.TextSearchResult, folderIdx: number) => {
const onResult = (result: TextSearchResult, folderIdx: number) => {
if (isCanceled) {
return;
}
@@ -79,14 +79,14 @@ export class TextSearchManager {
});
}
private resultSize(result: vscode.TextSearchResult): number {
const match = <vscode.TextSearchMatch>result;
private resultSize(result: TextSearchResult): number {
const match = <TextSearchMatch>result;
return Array.isArray(match.ranges) ?
match.ranges.length :
1;
}
private trimResultToSize(result: vscode.TextSearchMatch, size: number): vscode.TextSearchMatch {
private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch {
const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges];
const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches];
@@ -100,11 +100,11 @@ export class TextSearchManager {
};
}
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: vscode.TextSearchResult) => void, token: CancellationToken): Promise<vscode.TextSearchComplete | null | undefined> {
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise<TextSearchComplete | null | undefined> {
const queryTester = new QueryGlobTester(this.query, folderQuery);
const testingPs: Promise<void>[] = [];
const progress = {
report: (result: vscode.TextSearchResult) => {
report: (result: TextSearchResult) => {
if (!this.validateProviderResult(result)) {
return;
}
@@ -135,7 +135,7 @@ export class TextSearchManager {
});
}
private validateProviderResult(result: vscode.TextSearchResult): boolean {
private validateProviderResult(result: TextSearchResult): boolean {
if (extensionResultIsMatch(result)) {
if (Array.isArray(result.ranges)) {
if (!Array.isArray(result.preview.matches)) {
@@ -143,7 +143,7 @@ export class TextSearchManager {
return false;
}
if ((<vscode.Range[]>result.preview.matches).length !== result.ranges.length) {
if ((<Range[]>result.preview.matches).length !== result.ranges.length) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
@@ -159,22 +159,14 @@ export class TextSearchManager {
}
private readdir(dirname: string): Promise<string[]> {
return new Promise((resolve, reject) => {
this._extfs.readdir(dirname, (err, files) => {
if (err) {
return reject(err);
}
resolve(files);
});
});
return this._pfs.readdir(dirname);
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.TextSearchOptions {
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): TextSearchOptions {
const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern);
const options = <vscode.TextSearchOptions>{
const options = <TextSearchOptions>{
folder: URI.from(fq.folder),
excludes,
includes,
@@ -193,8 +185,8 @@ export class TextSearchManager {
}
}
function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery {
return <vscode.TextSearchQuery>{
function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery {
return <TextSearchQuery>{
isCaseSensitive: patternInfo.isCaseSensitive || false,
isRegExp: patternInfo.isRegExp || false,
isWordMatch: patternInfo.isWordMatch || false,
@@ -214,7 +206,7 @@ export class TextSearchResultsCollector {
this._batchedCollector = new BatchedCollector<IFileMatch>(512, items => this.sendItems(items));
}
add(data: vscode.TextSearchResult, folderIdx: number): void {
add(data: TextSearchResult, folderIdx: number): void {
// Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector.
// This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search
// providers that send results in random order. We could do this step afterwards instead.
@@ -251,8 +243,8 @@ export class TextSearchResultsCollector {
}
}
function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSearchResult {
// Warning: result from RipgrepTextSearchEH has fake vscode.Range. Don't depend on any other props beyond these...
function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult {
// Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these...
if (extensionResultIsMatch(data)) {
return <ITextSearchMatch>{
preview: {
@@ -279,8 +271,8 @@ function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSe
}
}
export function extensionResultIsMatch(data: vscode.TextSearchResult): data is vscode.TextSearchMatch {
return !!(<vscode.TextSearchMatch>data).preview;
export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch {
return !!(<TextSearchMatch>data).preview;
}
/**

View File

@@ -6,9 +6,8 @@
import * as assert from 'assert';
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/workbench/services/search/node/ripgrepSearchUtils';
import { fixRegexCRMatchingNonWordClass, fixRegexCRMatchingWhitespaceClass, fixRegexEndingPattern, fixRegexNewline, IRgMatch, IRgMessage, RipgrepParser, unicodeEscapesToPCRE2, fixNewline } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
import { TextSearchResult } from 'vscode';
import { Range, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes';
suite('RipgrepTextSearchEngine', () => {
test('unicodeEscapesToPCRE2', async () => {

View File

@@ -4,17 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { Progress } from 'vs/platform/progress/common/progress';
import { ITextQuery, QueryType } from 'vs/workbench/services/search/common/search';
import { ProviderResult, TextSearchComplete, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes';
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
import * as vscode from 'vscode';
suite('TextSearchManager', () => {
test('fixes encoding', async () => {
let correctEncoding = false;
const provider: vscode.TextSearchProvider = {
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextSearchComplete> {
const provider: TextSearchProvider = {
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress<TextSearchResult>, token: CancellationToken): ProviderResult<TextSearchComplete> {
correctEncoding = options.encoding === 'windows-1252';
return null;

View File

@@ -23,7 +23,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { RunOnceScheduler, timeout } from 'vs/base/common/async';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { hash } from 'vs/base/common/hash';
import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isLinux } from 'vs/base/common/platform';
@@ -88,7 +88,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
@IBackupFileService private readonly backupFileService: IBackupFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IHashService private readonly hashService: IHashService,
@ILogService private readonly logService: ILogService
) {
super(modelService, modeService);
@@ -215,7 +214,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Unset flags
const undo = this.setDirty(false);
let loadPromise: Promise<any>;
let loadPromise: Promise<unknown>;
if (soft) {
loadPromise = Promise.resolve();
} else {
@@ -734,7 +733,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this._onDidStateChange.fire(StateChange.SAVED);
// Telemetry
let telemetryPromise: Thenable<void>;
const settingsType = this.getTypeIfSettings();
if (settingsType) {
/* __GDPR__
@@ -743,22 +741,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
*/
this.telemetryService.publicLog('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data
telemetryPromise = Promise.resolve();
} else {
telemetryPromise = this.getTelemetryData(options.reason).then(data => {
/* __GDPR__
/* __GDPR__
"filePUT" : {
"${include}": [
"${FileTelemetryData}"
]
}
*/
this.telemetryService.publicLog('filePUT', data);
});
this.telemetryService.publicLog('filePUT', this.getTelemetryData(options.reason));
}
return telemetryPromise;
}, error => {
if (!error) {
error = new Error('Unknown Save Error'); // TODO@remote we should never get null as error (https://github.com/Microsoft/vscode/issues/55051)
@@ -824,32 +816,30 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return '';
}
private getTelemetryData(reason: number | undefined): Thenable<object> {
return this.hashService.createSHA1(this.resource.fsPath).then(hashedPath => {
const ext = extname(this.resource);
const fileName = basename(this.resource);
const telemetryData = {
mimeType: guessMimeTypes(this.resource.fsPath).join(', '),
ext,
path: hashedPath,
reason
};
private getTelemetryData(reason: number | undefined): object {
const ext = extname(this.resource);
const fileName = basename(this.resource);
const telemetryData = {
mimeType: guessMimeTypes(this.resource.fsPath).join(', '),
ext,
path: hash(this.resource.fsPath),
reason
};
if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
telemetryData['whitelistedjson'] = fileName;
if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) {
telemetryData['whitelistedjson'] = fileName;
}
/* __GDPR__FRAGMENT__
"FileTelemetryData" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
/* __GDPR__FRAGMENT__
"FileTelemetryData" : {
"mimeType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"ext": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"path": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"whitelistedjson": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
return telemetryData;
});
*/
return telemetryData;
}
private doTouch(versionId: number): Promise<void> {
@@ -918,7 +908,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
private onSaveError(error: any): void {
private onSaveError(error: Error): void {
// Prepare handler
if (!TextFileEditorModel.saveErrorHandler) {
@@ -1151,7 +1141,7 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler {
constructor(@INotificationService private readonly notificationService: INotificationService) { }
onSaveError(error: any, model: TextFileEditorModel): void {
onSaveError(error: Error, model: TextFileEditorModel): void {
this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.getResource()), toErrorMessage(error, false)));
}
}

View File

@@ -22,7 +22,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
@@ -50,7 +50,7 @@ export interface IBackupResult {
*/
export class TextFileService extends Disposable implements ITextFileService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<any>;
private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration> = this._register(new Emitter<IAutoSaveConfiguration>());
get onAutoSaveConfigurationChange(): Event<IAutoSaveConfiguration> { return this._onAutoSaveConfigurationChange.event; }
@@ -118,7 +118,7 @@ export class TextFileService extends Disposable implements ITextFileService {
isReadonly: streamContent.isReadonly,
size: streamContent.size,
value: res
} as IRawTextContent;
};
});
});
}
@@ -292,7 +292,7 @@ export class TextFileService extends Disposable implements ITextFileService {
}
private backupBeforeShutdown(dirtyToBackup: URI[], textFileEditorModelManager: ITextFileEditorModelManager, reason: ShutdownReason): Promise<IBackupResult> {
return this.windowsService.getWindowCount().then(windowCount => {
return this.windowsService.getWindowCount().then<IBackupResult>(windowCount => {
// When quit is requested skip the confirm callback and attempt to backup all workspaces.
// When quit is not requested the confirm callback should be shown when the window being
@@ -534,7 +534,7 @@ export class TextFileService extends Disposable implements ITextFileService {
saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise<ITextFileOperationResult>;
saveAll(resources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult>;
saveAll(arg1?: any, options?: ISaveOptions): Promise<ITextFileOperationResult> {
saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
// get all dirty
let toSave: URI[] = [];
@@ -649,9 +649,7 @@ export class TextFileService extends Disposable implements ITextFileService {
})).then(r => ({ results: mapResourceToResult.values() }));
}
private getFileModels(resources?: URI[]): ITextFileEditorModel[];
private getFileModels(resource?: URI): ITextFileEditorModel[];
private getFileModels(arg1?: any): ITextFileEditorModel[] {
private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] {
if (Array.isArray(arg1)) {
const models: ITextFileEditorModel[] = [];
(<URI[]>arg1).forEach(resource => {
@@ -664,10 +662,8 @@ export class TextFileService extends Disposable implements ITextFileService {
return this._models.getAll(<URI>arg1);
}
private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[];
private getDirtyFileModels(resource?: URI): ITextFileEditorModel[];
private getDirtyFileModels(arg1?: any): ITextFileEditorModel[] {
return this.getFileModels(arg1).filter(model => model.isDirty());
private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] {
return this.getFileModels(resources).filter(model => model.isDirty());
}
saveAs(resource: URI, target?: URI, options?: ISaveOptions): Promise<URI | undefined> {
@@ -748,7 +744,7 @@ export class TextFileService extends Disposable implements ITextFileService {
// Otherwise create the target file empty if it does not exist already and resolve it from there
else {
targetModelResolver = this.fileService.exists(target).then<any>(exists => {
targetModelResolver = this.fileService.exists(target).then(exists => {
targetExists = exists;
// create target model adhoc if file does not exist yet
@@ -756,7 +752,7 @@ export class TextFileService extends Disposable implements ITextFileService {
return this.fileService.updateContent(target, '');
}
return undefined;
return Promise.resolve(undefined);
}).then(() => this.models.loadOrCreate(target));
}
@@ -899,13 +895,13 @@ export class TextFileService extends Disposable implements ITextFileService {
}
move(source: URI, target: URI, overwrite?: boolean): Promise<void> {
const waitForPromises: Promise<any>[] = [];
const waitForPromises: Promise<unknown>[] = [];
// Event
this._onWillMove.fire({
oldResource: source,
newResource: target,
waitUntil(promise: Promise<any>) {
waitUntil(promise: Promise<unknown>) {
waitForPromises.push(promise.then(undefined, errors.onUnexpectedError));
}
});
@@ -916,7 +912,7 @@ export class TextFileService extends Disposable implements ITextFileService {
return Promise.all(waitForPromises).then(() => {
// Handle target models if existing (if target URI is a folder, this can be multiple)
let handleTargetModelPromise: Promise<any> = Promise.resolve();
let handleTargetModelPromise: Promise<unknown> = Promise.resolve();
const dirtyTargetModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */));
if (dirtyTargetModels.length) {
handleTargetModelPromise = this.revertAll(dirtyTargetModels.map(targetModel => targetModel.getResource()), { soft: true });
@@ -925,7 +921,7 @@ export class TextFileService extends Disposable implements ITextFileService {
return handleTargetModelPromise.then(() => {
// Handle dirty source models if existing (if source URI is a folder, this can be multiple)
let handleDirtySourceModels: Promise<any>;
let handleDirtySourceModels: Promise<unknown>;
const dirtySourceModels = this.getDirtyFileModels().filter(model => isEqualOrParent(model.getResource(), source, !platform.isLinux /* ignorecase */));
const dirtyTargetModels: URI[] = [];
if (dirtySourceModels.length) {

View File

@@ -265,7 +265,7 @@ export interface IResolvedTextFileEditorModel extends ITextFileEditorModel {
export interface IWillMoveEvent {
oldResource: URI;
newResource: URI;
waitUntil(p: Promise<any>): void;
waitUntil(p: Promise<unknown>): void;
}
export interface ITextFileService extends IDisposable {

View File

@@ -58,13 +58,40 @@ export class ColorThemeData implements IColorTheme {
}
get tokenColors(): ITokenColorizationRule[] {
const result: ITokenColorizationRule[] = [];
// the default rule (scope empty) is always the first rule. Ignore all other default rules.
const foreground = this.getColor(editorForeground) || this.getDefault(editorForeground)!;
const background = this.getColor(editorBackground) || this.getDefault(editorBackground)!;
result.push({
settings: {
foreground: Color.Format.CSS.formatHexA(foreground),
background: Color.Format.CSS.formatHexA(background)
}
});
let hasDefaultTokens = false;
function addRule(rule: ITokenColorizationRule) {
if (rule.scope && rule.settings) {
if (rule.scope === 'token.info-token') {
hasDefaultTokens = true;
}
result.push(rule);
}
}
this.themeTokenColors.forEach(addRule);
// Add the custom colors after the theme colors
// so that they will override them
return this.themeTokenColors.concat(this.customTokenColors);
this.customTokenColors.forEach(addRule);
if (!hasDefaultTokens) {
defaultThemeColors[this.type].forEach(addRule);
}
return result;
}
public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined {
let color: Color | undefined = this.customColorMap[colorId];
if (color) {
@@ -93,9 +120,6 @@ export class ColorThemeData implements IColorTheme {
if (types.isObject(themeSpecificColors)) {
this.overwriteCustomColors(themeSpecificColors);
}
if (this.themeTokenColors && this.themeTokenColors.length) {
updateDefaultRuleSettings(this.themeTokenColors[0], this);
}
}
private overwriteCustomColors(colors: IColorCustomizations) {
@@ -155,32 +179,13 @@ export class ColorThemeData implements IColorTheme {
if (!this.location) {
return Promise.resolve(undefined);
}
this.themeTokenColors = [];
this.colorMap = {};
return _loadColorTheme(fileService, this.location, this.themeTokenColors, this.colorMap).then(_ => {
this.isLoaded = true;
this.sanitizeTokenColors();
});
}
/**
* Place the default settings first and add the token-info rules
*/
private sanitizeTokenColors() {
let hasDefaultTokens = false;
let updatedTokenColors: ITokenColorizationRule[] = [updateDefaultRuleSettings({ settings: {} }, this)];
this.themeTokenColors.forEach(rule => {
if (rule.scope && rule.settings) {
if (rule.scope === 'token.info-token') {
hasDefaultTokens = true;
}
updatedTokenColors.push(rule);
}
});
if (!hasDefaultTokens) {
updatedTokenColors.push(...defaultThemeColors[this.type]);
}
this.themeTokenColors = updatedTokenColors;
}
toStorageData() {
let colorMapData = {};
for (let key in this.colorMap) {
@@ -200,7 +205,7 @@ export class ColorThemeData implements IColorTheme {
}
hasEqualData(other: ColorThemeData) {
return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.tokenColors, other.tokenColors);
return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.themeTokenColors, other.themeTokenColors);
}
get baseTheme(): string {
@@ -220,7 +225,7 @@ export class ColorThemeData implements IColorTheme {
static createUnloadedTheme(id: string): ColorThemeData {
let themeData = new ColorThemeData(id, '', '__' + id);
themeData.isLoaded = false;
themeData.themeTokenColors = [{ settings: {} }];
themeData.themeTokenColors = [];
themeData.watch = false;
return themeData;
}
@@ -228,7 +233,7 @@ export class ColorThemeData implements IColorTheme {
static createLoadedEmptyTheme(id: string, settingsId: string): ColorThemeData {
let themeData = new ColorThemeData(id, '', settingsId);
themeData.isLoaded = true;
themeData.themeTokenColors = [{ settings: {} }];
themeData.themeTokenColors = [];
themeData.watch = false;
return themeData;
}
@@ -357,14 +362,6 @@ function _loadSyntaxTokens(fileService: IFileService, themeLocation: URI, result
});
}
function updateDefaultRuleSettings(defaultRule: ITokenColorizationRule, theme: ColorThemeData): ITokenColorizationRule {
const foreground = theme.getColor(editorForeground) || theme.getDefault(editorForeground)!;
const background = theme.getColor(editorBackground) || theme.getDefault(editorBackground)!;
defaultRule.settings.foreground = Color.Format.CSS.formatHexA(foreground);
defaultRule.settings.background = Color.Format.CSS.formatHexA(background);
return defaultRule;
}
let defaultThemeColors: { [baseTheme: string]: ITokenColorizationRule[] } = {
'light': [
{ scope: 'token.info-token', settings: { foreground: '#316bcd' } },

View File

@@ -17,7 +17,7 @@ import { ColorThemeData } from './colorThemeData';
import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService';
import { Event, Emitter } from 'vs/base/common/event';
import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ColorThemeStore } from 'vs/workbench/services/themes/browser/colorThemeStore';
import { FileIconThemeStore } from 'vs/workbench/services/themes/common/fileIconThemeStore';
import { FileIconThemeData } from 'vs/workbench/services/themes/common/fileIconThemeData';
@@ -77,11 +77,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
private container: HTMLElement;
private readonly onColorThemeChange: Emitter<IColorTheme>;
private watchedColorThemeLocation: URI | undefined;
private watchedColorThemeDisposable: IDisposable;
private iconThemeStore: FileIconThemeStore;
private currentIconTheme: FileIconThemeData;
private readonly onFileIconThemeChange: Emitter<IFileIconTheme>;
private watchedIconThemeLocation: URI | undefined;
private watchedIconThemeDisposable: IDisposable;
private themingParticipantChangeListener: IDisposable;
@@ -264,7 +266,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
if (devThemes.length) {
return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY);
} else {
return this.setFileIconTheme(theme && theme.id, undefined);
return this.setFileIconTheme(theme && theme.id || DEFAULT_ICON_THEME_SETTING_VALUE, undefined);
}
});
}),
@@ -287,7 +289,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
let iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);
if (iconThemeSetting !== this.currentIconTheme.settingsId) {
this.iconThemeStore.findThemeBySettingsId(iconThemeSetting).then(theme => {
this.setFileIconTheme(theme && theme.id, undefined);
this.setFileIconTheme(theme && theme.id || DEFAULT_ICON_THEME_SETTING_VALUE, undefined);
});
}
}
@@ -395,13 +397,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}
if (this.fileService && !resources.isEqual(newTheme.location, this.watchedColorThemeLocation)) {
if (this.watchedColorThemeLocation) {
this.fileService.unwatch(this.watchedColorThemeLocation);
this.watchedColorThemeLocation = undefined;
}
dispose(this.watchedColorThemeDisposable);
this.watchedColorThemeLocation = undefined;
if (newTheme.location && (newTheme.watch || !!this.environmentService.extensionDevelopmentLocationURI)) {
this.watchedColorThemeLocation = newTheme.location;
this.fileService.watch(this.watchedColorThemeLocation);
this.watchedColorThemeDisposable = this.fileService.watch(newTheme.location);
}
}
@@ -513,13 +514,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}
if (this.fileService && !resources.isEqual(iconThemeData.location, this.watchedIconThemeLocation)) {
if (this.watchedIconThemeLocation) {
this.fileService.unwatch(this.watchedIconThemeLocation);
this.watchedIconThemeLocation = undefined;
}
dispose(this.watchedIconThemeDisposable);
this.watchedIconThemeLocation = undefined;
if (iconThemeData.location && (iconThemeData.watch || !!this.environmentService.extensionDevelopmentLocationURI)) {
this.watchedIconThemeLocation = iconThemeData.location;
this.fileService.watch(this.watchedIconThemeLocation);
this.watchedIconThemeDisposable = this.fileService.watch(iconThemeData.location);
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator, IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import * as arrays from 'vs/base/common/arrays';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IFilesConfiguration } from 'vs/platform/files/common/files';
@@ -113,7 +113,7 @@ export interface IUntitledEditorService {
export class UntitledEditorService extends Disposable implements IUntitledEditorService {
_serviceBrand: any;
_serviceBrand: ServiceIdentifier<any>;
private mapResourceToInput = new ResourceMap<UntitledEditorInput>();
private mapResourceToAssociatedFilePath = new ResourceMap<boolean>();

View File

@@ -10,7 +10,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/
import { IWindowService, MessageBoxOptions, IWindowsService } from 'vs/platform/windows/common/windows';
import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { StorageService } from 'vs/platform/storage/node/storageService';
import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
@@ -203,7 +203,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
// If we are in no-workspace or single-folder workspace, adding folders has to
// enter a workspace.
if (state !== WorkbenchState.WORKSPACE) {
let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData));
let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri }));
newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd);
newWorkspaceFolders = distinct(newWorkspaceFolders, folder => getComparisonKey(folder.uri));
@@ -334,64 +334,37 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
);
}
enterWorkspace(path: URI): Promise<void> {
async enterWorkspace(path: URI): Promise<void> {
if (!!this.environmentService.extensionTestsLocationURI) {
return Promise.reject(new Error('Entering a new workspace is not possible in tests.'));
}
const workspace = await this.workspaceService.getWorkspaceIdentifier(path);
// Settings migration (only if we come from a folder workspace)
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
await this.migrateWorkspaceSettings(workspace);
}
const workspaceImpl = this.contextService as WorkspaceService;
await workspaceImpl.initialize(workspace);
// Restart extension host if first root folder changed (impact on deprecated workspace.rootPath API)
// Stop the extension host first to give extensions most time to shutdown
this.extensionService.stopExtensionHost();
let extensionHostStarted: boolean = false;
const startExtensionHost = () => {
if (this.windowService.getConfiguration().remoteAuthority) {
this.windowService.reloadWindow(); // TODO aeschli: workaround until restarting works
const result = await this.windowService.enterWorkspace(path);
if (result) {
await this.migrateStorage(result.workspace);
// Reinitialize backup service
if (this.backupFileService instanceof BackupFileService) {
this.backupFileService.initialize(result.backupPath!);
}
}
if (this.windowService.getConfiguration().remoteAuthority) {
this.windowService.reloadWindow(); // TODO aeschli: workaround until restarting works
} else {
this.extensionService.startExtensionHost();
extensionHostStarted = true;
};
return this.windowService.enterWorkspace(path).then(result => {
// Migrate storage and settings if we are to enter a workspace
if (result) {
return this.migrate(result.workspace).then(() => {
// Reinitialize backup service
if (this.backupFileService instanceof BackupFileService) {
this.backupFileService.initialize(result.backupPath!);
}
// Reinitialize configuration service
const workspaceImpl = this.contextService as WorkspaceService;
return workspaceImpl.initialize(result.workspace, startExtensionHost);
});
}
return Promise.resolve();
}).then(undefined, error => {
if (!extensionHostStarted) {
startExtensionHost(); // start the extension host if not started
}
return Promise.reject(error);
});
}
private migrate(toWorkspace: IWorkspaceIdentifier): Promise<void> {
// Storage migration
return this.migrateStorage(toWorkspace).then(() => {
// Settings migration (only if we come from a folder workspace)
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
return this.migrateWorkspaceSettings(toWorkspace);
}
return undefined;
});
}
}
private migrateStorage(toWorkspace: IWorkspaceIdentifier): Promise<void> {