mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-14 12:08:36 -05:00
Merge from vscode 2b0b9136329c181a9e381463a1f7dc3a2d105a34 (#4880)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class NativeDialogService implements IDialogService {
|
||||
return {
|
||||
confirmed: buttonIndexMap[result.button] === 0 ? true : false,
|
||||
checkboxChecked: result.checkboxChecked
|
||||
} as IConfirmationResult;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface IVisibleEditor extends IEditor {
|
||||
}
|
||||
|
||||
export interface IEditorService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
160
src/vs/workbench/services/files2/common/workspaceWatcher.ts
Normal file
160
src/vs/workbench/services/files2/common/workspaceWatcher.ts
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }]);
|
||||
});
|
||||
@@ -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>;
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
@@ -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 },
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
413
src/vs/workbench/services/search/common/searchExtTypes.ts
Normal file
413
src/vs/workbench/services/search/common/searchExtTypes.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' } },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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> {
|
||||
Reference in New Issue
Block a user