mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode cfbd1999769f4f08dce29629fb92fdc0fac53829
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent, AuthenticationProviderInformation } from 'vs/editor/common/modes';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -15,6 +15,9 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }
|
||||
|
||||
export const IAuthenticationService = createDecorator<IAuthenticationService>('IAuthenticationService');
|
||||
|
||||
@@ -68,12 +71,17 @@ export interface SessionRequestInfo {
|
||||
[scopes: string]: SessionRequest;
|
||||
}
|
||||
|
||||
CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) {
|
||||
const environmentService = accessor.get(IWorkbenchEnvironmentService);
|
||||
return environmentService.options?.codeExchangeProxyEndpoints;
|
||||
});
|
||||
|
||||
export class AuthenticationService extends Disposable implements IAuthenticationService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private _placeholderMenuItem: IDisposable | undefined;
|
||||
private _noAccountsMenuItem: IDisposable | undefined;
|
||||
private _signInRequestItems = new Map<string, SessionRequestInfo>();
|
||||
private _badgeDisposable: IDisposable | undefined;
|
||||
private _accountBadgeDisposable = this._register(new MutableDisposable());
|
||||
|
||||
private _authenticationProviders: Map<string, MainThreadAuthenticationProvider> = new Map<string, MainThreadAuthenticationProvider>();
|
||||
|
||||
@@ -203,10 +211,9 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
if (this._signInRequestItems.size === 0) {
|
||||
this._badgeDisposable?.dispose();
|
||||
this._badgeDisposable = undefined;
|
||||
} else {
|
||||
this._accountBadgeDisposable.clear();
|
||||
|
||||
if (this._signInRequestItems.size > 0) {
|
||||
let numberOfRequests = 0;
|
||||
this._signInRequestItems.forEach(providerRequests => {
|
||||
Object.keys(providerRequests).forEach(request => {
|
||||
@@ -215,7 +222,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
||||
});
|
||||
|
||||
const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested"));
|
||||
this._badgeDisposable = this.activityService.showAccountsActivity({ badge });
|
||||
this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +291,8 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
||||
});
|
||||
}
|
||||
|
||||
this._accountBadgeDisposable.clear();
|
||||
|
||||
let numberOfRequests = 0;
|
||||
this._signInRequestItems.forEach(providerRequests => {
|
||||
Object.keys(providerRequests).forEach(request => {
|
||||
@@ -292,7 +301,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
||||
});
|
||||
|
||||
const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested"));
|
||||
this._badgeDisposable = this.activityService.showAccountsActivity({ badge });
|
||||
this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge });
|
||||
}
|
||||
}
|
||||
getLabel(id: string): string {
|
||||
|
||||
@@ -99,7 +99,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]);
|
||||
}
|
||||
this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas()));
|
||||
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties)));
|
||||
this._register(configurationRegistry.onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties)));
|
||||
|
||||
this.workspaceEditingQueue = new Queue<void>();
|
||||
}
|
||||
@@ -510,6 +510,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
const folderConfiguration = this.cachedFolderConfigs.get(this.workspace.folders[0].uri);
|
||||
if (folderConfiguration) {
|
||||
this._configuration.updateWorkspaceConfiguration(folderConfiguration.reprocess());
|
||||
this._configuration.updateFolderConfiguration(this.workspace.folders[0].uri, folderConfiguration.reprocess());
|
||||
}
|
||||
} else {
|
||||
this._configuration.updateWorkspaceConfiguration(this.workspaceConfiguration.reprocessWorkspaceSettings());
|
||||
|
||||
@@ -112,7 +112,7 @@ suite.skip('WorkspaceContextService - Folder', () => { // {{SQL CARBON EDIT}} sk
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, new DiskFileSystemProvider(new NullLogService()), environmentService, new NullLogService()));
|
||||
workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService(), { _serviceBrand: undefined, ...product }));
|
||||
workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, { _serviceBrand: undefined, ...product }, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService()));
|
||||
return (<WorkspaceService>workspaceContextService).initialize(convertToWorkspacePayload(URI.file(folderDir)));
|
||||
});
|
||||
});
|
||||
@@ -882,56 +882,140 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI
|
||||
});
|
||||
});
|
||||
|
||||
test('application settings are not read from workspace', () => {
|
||||
test('application settings are not read from workspace', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting": "workspaceValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue'));
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue');
|
||||
});
|
||||
|
||||
test('machine settings are not read from workspace', () => {
|
||||
test('application settings are not read from workspace when workspace folder uri is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting": "workspaceValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('machine settings are not read from workspace', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting": "workspaceValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => assert.equal(testObject.getValue('configurationService.folder.machineSetting'), 'userValue'));
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('get application scope settings are not loaded after defaults are registered', () => {
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.anotherApplicationSetting": "workspaceValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.folder.anotherApplicationSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.deepEqual(testObject.keys().workspace, []);
|
||||
});
|
||||
test('machine settings are not read from workspace when workspace folder uri is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting": "workspaceValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('get machine scope settings are not loaded after defaults are registered', () => {
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.anotherMachineSetting": "workspaceValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.folder.anotherMachineSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.deepEqual(testObject.keys().workspace, []);
|
||||
});
|
||||
test('get application scope settings are not loaded after defaults are registered', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting-2": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting-2": "workspaceValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting-2'), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.folder.applicationSetting-2': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting-2'), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting-2'), 'userValue');
|
||||
});
|
||||
|
||||
test('get application scope settings are not loaded after defaults are registered when workspace folder uri is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting-3": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.applicationSetting-3": "workspaceValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.folder.applicationSetting-3': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('get machine scope settings are not loaded after defaults are registered', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting-2": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting-2": "workspaceValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting-2'), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.folder.machineSetting-2': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting-2'), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting-2'), 'userValue');
|
||||
});
|
||||
|
||||
test('get machine scope settings are not loaded after defaults are registered when workspace folder uri is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting-3": "userValue" }');
|
||||
fs.writeFileSync(path.join(workspaceDir, '.vscode', 'settings.json'), '{ "configurationService.folder.machineSetting-3": "workspaceValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.folder.machineSetting-3': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('reload configuration emits events after global configuraiton changes', () => {
|
||||
@@ -1231,111 +1315,202 @@ suite.skip('WorkspaceConfigurationService-Multiroot', () => { // {{SQL CARBON ED
|
||||
return undefined;
|
||||
});
|
||||
|
||||
test('application settings are not read from workspace', () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.applicationSetting": "userValue" }');
|
||||
return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true)
|
||||
.then(() => testObject.reloadConfiguration())
|
||||
.then(() => assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue'));
|
||||
test('application settings are not read from workspace', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }');
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting'), 'userValue');
|
||||
});
|
||||
|
||||
test('machine settings are not read from workspace', () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.machineSetting": "userValue" }');
|
||||
return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true)
|
||||
.then(() => testObject.reloadConfiguration())
|
||||
.then(() => assert.equal(testObject.getValue('configurationService.workspace.machineSetting'), 'userValue'));
|
||||
test('application settings are not read from workspace when folder is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.applicationSetting": "userValue" }');
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.applicationSetting': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('workspace settings override user settings after defaults are registered ', () => {
|
||||
test('machine settings are not read from workspace', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }');
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting'), 'userValue');
|
||||
});
|
||||
|
||||
test('machine settings are not read from workspace when folder is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.machineSetting": "userValue" }');
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.machineSetting': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.folder.machineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('get application scope settings are not loaded after defaults are registered', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.newSetting": "userValue" }');
|
||||
return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting': 'workspaceValue' } }], true)
|
||||
.then(() => testObject.reloadConfiguration())
|
||||
.then(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.newSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet'
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'workspaceValue');
|
||||
});
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.newSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting'), 'userValue');
|
||||
});
|
||||
|
||||
test('workspace settings override user settings after defaults are registered for machine overridable settings ', () => {
|
||||
test('get application scope settings are not loaded after defaults are registered when workspace folder is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.newSetting-2": "userValue" }');
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newSetting-2': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting-2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.newSetting-2': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting-2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newSetting-2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('workspace settings override user settings after defaults are registered for machine overridable settings ', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.newMachineOverridableSetting": "userValue" }');
|
||||
return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newMachineOverridableSetting': 'workspaceValue' } }], true)
|
||||
.then(() => testObject.reloadConfiguration())
|
||||
.then(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.newMachineOverridableSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE_OVERRIDABLE
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue');
|
||||
});
|
||||
await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ path: ['settings'], value: { 'configurationService.workspace.newMachineOverridableSetting': 'workspaceValue' } }], true);
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.newMachineOverridableSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE_OVERRIDABLE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.newMachineOverridableSetting'), 'workspaceValue');
|
||||
|
||||
});
|
||||
|
||||
test('application settings are not read from workspace folder', () => {
|
||||
test('application settings are not read from workspace folder', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.applicationSetting": "userValue" }');
|
||||
fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue'));
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.applicationSetting'), 'userValue');
|
||||
});
|
||||
|
||||
test('machine settings are not read from workspace folder', () => {
|
||||
test('application settings are not read from workspace folder when workspace folder is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.applicationSetting": "userValue" }');
|
||||
fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.applicationSetting": "workspaceFolderValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.applicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('machine settings are not read from workspace folder', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.machineSetting": "userValue" }');
|
||||
fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.machineSetting": "workspaceFolderValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => assert.equal(testObject.getValue('configurationService.workspace.machineSetting'), 'userValue'));
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.machineSetting'), 'userValue');
|
||||
});
|
||||
|
||||
test('application settings are not read from workspace folder after defaults are registered', () => {
|
||||
test('machine settings are not read from workspace folder when workspace folder is passed', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.machineSetting": "userValue" }');
|
||||
fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.machineSetting": "workspaceFolderValue" }');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.machineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('application settings are not read from workspace folder after defaults are registered', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.testNewApplicationSetting": "userValue" }');
|
||||
fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewApplicationSetting": "workspaceFolderValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.testNewApplicationSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceFolderValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.testNewApplicationSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewApplicationSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('application settings are not read from workspace folder after defaults are registered', () => {
|
||||
test('application settings are not read from workspace folder after defaults are registered', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.workspace.testNewMachineSetting": "userValue" }');
|
||||
fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewMachineSetting": "workspaceFolderValue" }');
|
||||
return testObject.reloadConfiguration()
|
||||
.then(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.testNewMachineSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
await testObject.reloadConfiguration();
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceFolderValue');
|
||||
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationService.workspace.testNewMachineSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
|
||||
await testObject.reloadConfiguration();
|
||||
assert.equal(testObject.getValue('configurationService.workspace.testNewMachineSetting', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'userValue');
|
||||
});
|
||||
|
||||
test('resource setting in folder is read after it is registered later', () => {
|
||||
|
||||
@@ -17,8 +17,8 @@ export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmen
|
||||
|
||||
readonly configuration: INativeEnvironmentConfiguration;
|
||||
|
||||
readonly disableCrashReporter: boolean;
|
||||
readonly crashReporterDirectory?: string;
|
||||
readonly crashReporterId?: string;
|
||||
|
||||
readonly cliPath: string;
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
|
||||
interface IScannedBuiltinExtension {
|
||||
extensionPath: string,
|
||||
packageJSON: IExtensionManifest,
|
||||
packageNLSPath?: string,
|
||||
readmePath?: string,
|
||||
changelogPath?: string,
|
||||
extensionPath: string;
|
||||
packageJSON: IExtensionManifest;
|
||||
packageNLS?: any;
|
||||
readmePath?: string;
|
||||
changelogPath?: string;
|
||||
}
|
||||
|
||||
export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScannerService {
|
||||
@@ -54,7 +54,7 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne
|
||||
location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath),
|
||||
type: ExtensionType.System,
|
||||
packageJSON: e.packageJSON,
|
||||
packageNLSUrl: e.packageNLSPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.packageNLSPath) : undefined,
|
||||
packageNLS: e.packageNLS,
|
||||
readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined,
|
||||
changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined,
|
||||
}));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtension, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtension, IScannedExtension, ExtensionType, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
@@ -128,6 +128,7 @@ export interface IExtensionRecommendationsService {
|
||||
|
||||
getAllRecommendationsWithReason(): IStringDictionary<IExtensionRecommendationReson>;
|
||||
getFileBasedRecommendations(): IExtensionRecommendation[];
|
||||
getImportantRecommendations(): Promise<IExtensionRecommendation[]>;
|
||||
getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]>;
|
||||
getOtherRecommendations(): Promise<IExtensionRecommendation[]>;
|
||||
getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]>;
|
||||
@@ -145,6 +146,7 @@ export const IWebExtensionsScannerService = createDecorator<IWebExtensionsScanne
|
||||
export interface IWebExtensionsScannerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]>;
|
||||
scanAndTranslateExtensions(type?: ExtensionType): Promise<ITranslatedScannedExtension[]>;
|
||||
addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension>;
|
||||
removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionType, IExtensionIdentifier, IExtensionManifest, IScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionType, IExtensionIdentifier, IExtensionManifest, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRequestService, isSuccess, asText } from 'vs/platform/request/common/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
@@ -33,14 +30,13 @@ export class WebExtensionManagementService extends Disposable implements IExtens
|
||||
|
||||
constructor(
|
||||
@IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> {
|
||||
const extensions = await this.webExtensionsScannerService.scanExtensions(type);
|
||||
const extensions = await this.webExtensionsScannerService.scanAndTranslateExtensions(type);
|
||||
return Promise.all(extensions.map(e => this.toLocalExtension(e)));
|
||||
}
|
||||
|
||||
@@ -83,23 +79,11 @@ export class WebExtensionManagementService extends Disposable implements IExtens
|
||||
return userExtensions.find(e => areSameExtensions(e.identifier, identifier));
|
||||
}
|
||||
|
||||
private async toLocalExtension(scannedExtension: IScannedExtension): Promise<ILocalExtension> {
|
||||
let manifest = scannedExtension.packageJSON;
|
||||
if (scannedExtension.packageNLSUrl) {
|
||||
try {
|
||||
const context = await this.requestService.request({ type: 'GET', url: scannedExtension.packageNLSUrl.toString() }, CancellationToken.None);
|
||||
if (isSuccess(context)) {
|
||||
const content = await asText(context);
|
||||
if (content) {
|
||||
manifest = localizeManifest(manifest, JSON.parse(content));
|
||||
}
|
||||
}
|
||||
} catch (error) { /* ignore */ }
|
||||
}
|
||||
private async toLocalExtension(scannedExtension: ITranslatedScannedExtension): Promise<ILocalExtension> {
|
||||
return <ILocalExtension>{
|
||||
type: scannedExtension.type,
|
||||
identifier: scannedExtension.identifier,
|
||||
manifest,
|
||||
manifest: scannedExtension.packageJSON,
|
||||
location: scannedExtension.location,
|
||||
isMachineScoped: false,
|
||||
publisherId: null,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as semver from 'semver-umd';
|
||||
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
@@ -21,6 +21,9 @@ import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extens
|
||||
import { groupByExtension, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStaticExtension } from 'vs/workbench/workbench.web.api';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
|
||||
|
||||
interface IUserExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
@@ -44,7 +47,7 @@ const AssetTypeWebResource = 'Microsoft.VisualStudio.Code.WebResources';
|
||||
|
||||
function getExtensionLocation(assetUri: URI): URI { return joinPath(assetUri, AssetTypeWebResource, 'extension'); }
|
||||
|
||||
export class WebExtensionsScannerService implements IWebExtensionsScannerService {
|
||||
export class WebExtensionsScannerService extends Disposable implements IWebExtensionsScannerService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -53,6 +56,8 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
||||
private readonly extensionsResource: URI | undefined = undefined;
|
||||
private readonly userExtensionsResourceLimiter: Queue<IUserExtension[]> = new Queue<IUserExtension[]>();
|
||||
|
||||
private userExtensionsPromise: Promise<IScannedExtension[]> | undefined;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,
|
||||
@@ -61,22 +66,48 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
if (isWeb) {
|
||||
this.extensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json');
|
||||
this.systemExtensionsPromise = this.builtinExtensionsScannerService.scanBuiltinExtensions();
|
||||
this.systemExtensionsPromise = this.readSystemExtensions();
|
||||
this.defaultExtensionsPromise = this.readDefaultExtensions();
|
||||
if (this.extensionsResource) {
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.extensionsResource!))(() => this.userExtensionsPromise = undefined));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async readDefaultExtensions(): Promise<IScannedExtension[]> {
|
||||
private async readSystemExtensions(): Promise<IScannedExtension[]> {
|
||||
const extensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();
|
||||
return extensions.concat(this.getStaticExtensions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* All extensions defined via `staticExtensions`
|
||||
*/
|
||||
private getStaticExtensions(builtin: boolean): IScannedExtension[] {
|
||||
const staticExtensions = this.environmentService.options && Array.isArray(this.environmentService.options.staticExtensions) ? this.environmentService.options.staticExtensions : [];
|
||||
return (
|
||||
staticExtensions
|
||||
.filter(e => Boolean(e.isBuiltin) === builtin)
|
||||
.map(e => ({
|
||||
identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) },
|
||||
location: e.extensionLocation,
|
||||
type: ExtensionType.System,
|
||||
packageJSON: e.packageJSON,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
private async readDefaultExtensions(): Promise<IScannedExtension[]> {
|
||||
const defaultUserWebExtensions = await this.readDefaultUserWebExtensions();
|
||||
return [...staticExtensions, ...defaultUserWebExtensions].map<IScannedExtension>(e => ({
|
||||
const extensions = defaultUserWebExtensions.map(e => ({
|
||||
identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) },
|
||||
location: e.extensionLocation,
|
||||
type: ExtensionType.User,
|
||||
packageJSON: e.packageJSON,
|
||||
}));
|
||||
return extensions.concat(this.getStaticExtensions(false));
|
||||
}
|
||||
|
||||
private async readDefaultUserWebExtensions(): Promise<IStaticExtension[]> {
|
||||
@@ -113,12 +144,52 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
||||
if (type === undefined || type === ExtensionType.User) {
|
||||
const staticExtensions = await this.defaultExtensionsPromise;
|
||||
extensions.push(...staticExtensions);
|
||||
const userExtensions = await this.scanUserExtensions();
|
||||
if (!this.userExtensionsPromise) {
|
||||
this.userExtensionsPromise = this.scanUserExtensions();
|
||||
}
|
||||
const userExtensions = await this.userExtensionsPromise;
|
||||
extensions.push(...userExtensions);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async scanAndTranslateExtensions(type?: ExtensionType): Promise<ITranslatedScannedExtension[]> {
|
||||
const extensions = await this.scanExtensions(type);
|
||||
return Promise.all(extensions.map((ext) => this._translateScannedExtension(ext)));
|
||||
}
|
||||
|
||||
private async _translateScannedExtension(scannedExtension: IScannedExtension): Promise<ITranslatedScannedExtension> {
|
||||
let manifest = scannedExtension.packageJSON;
|
||||
if (scannedExtension.packageNLS) {
|
||||
// package.nls.json is inlined
|
||||
try {
|
||||
manifest = localizeManifest(manifest, scannedExtension.packageNLS);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
/* ignore */
|
||||
}
|
||||
} else if (scannedExtension.packageNLSUrl) {
|
||||
// package.nls.json needs to be fetched
|
||||
try {
|
||||
const context = await this.requestService.request({ type: 'GET', url: scannedExtension.packageNLSUrl.toString() }, CancellationToken.None);
|
||||
if (isSuccess(context)) {
|
||||
const content = await asText(context);
|
||||
if (content) {
|
||||
manifest = localizeManifest(manifest, JSON.parse(content));
|
||||
}
|
||||
}
|
||||
} catch (error) { /* ignore */ }
|
||||
}
|
||||
return {
|
||||
identifier: scannedExtension.identifier,
|
||||
location: scannedExtension.location,
|
||||
type: scannedExtension.type,
|
||||
packageJSON: manifest,
|
||||
readmeUrl: scannedExtension.readmeUrl,
|
||||
changelogUrl: scannedExtension.changelogUrl
|
||||
};
|
||||
}
|
||||
|
||||
async addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension> {
|
||||
if (!galleryExtension.assetTypes.some(type => type.startsWith(AssetTypeWebResource))) {
|
||||
throw new Error(`Missing ${AssetTypeWebResource} asset type`);
|
||||
@@ -149,7 +220,7 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
||||
|
||||
async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void> {
|
||||
let userExtensions = await this.readUserExtensions();
|
||||
userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && version ? extension.version === version : true));
|
||||
userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && (version ? extension.version === version : true)));
|
||||
await this.writeUserExtensions(userExtensions);
|
||||
}
|
||||
|
||||
@@ -226,6 +297,7 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
||||
packageNLSUri: e.packageNLSUri?.toJSON(),
|
||||
}));
|
||||
await this.fileService.writeFile(this.extensionsResource!, VSBuffer.fromString(JSON.stringify(storedUserExtensions)));
|
||||
this.userExtensionsPromise = undefined;
|
||||
return userExtensions;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -397,7 +397,7 @@ suite('ExtensionEnablementService Test', () => {
|
||||
assert.ok(!testObject.canChangeEnablement(extension));
|
||||
});
|
||||
|
||||
test('test extension is disabled when disabled in enviroment', async () => {
|
||||
test('test extension is disabled when disabled in environment', async () => {
|
||||
const extension = aLocalExtension('pub.a');
|
||||
instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: ['pub.a'] } as IWorkbenchEnvironmentService);
|
||||
instantiationService.stub(IExtensionManagementService, { onDidUninstallExtension: didUninstallEvent.event, getInstalled: () => Promise.resolve([extension, aLocalExtension('pub.b')]) } as IExtensionManagementService);
|
||||
|
||||
@@ -17,19 +17,19 @@ import { AbstractExtensionService, parseScannedExtension } from 'vs/workbench/se
|
||||
import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost';
|
||||
import { canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, IExtensionDescription, ExtensionKind } from 'vs/platform/extensions/common/extensions';
|
||||
import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { DeltaExtensionsResult } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
|
||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
|
||||
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
|
||||
|
||||
private _disposables = new DisposableStore();
|
||||
private _remoteInitData: IRemoteExtensionHostInitData | null = null;
|
||||
private _runningLocation: Map<string, ExtensionRunningLocation>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@@ -54,6 +54,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
productService,
|
||||
);
|
||||
|
||||
this._runningLocation = new Map<string, ExtensionRunningLocation>();
|
||||
|
||||
this._initialize();
|
||||
this._initFetchFileSystem();
|
||||
}
|
||||
@@ -73,10 +75,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
return {
|
||||
getInitData: async () => {
|
||||
const allExtensions = await this.getExtensions();
|
||||
const webExtensions = allExtensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService));
|
||||
const localWebWorkerExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker);
|
||||
return {
|
||||
autoStart: true,
|
||||
extensions: webExtensions
|
||||
extensions: localWebWorkerExtensions
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -109,32 +111,26 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
|
||||
protected async _scanAndHandleExtensions(): Promise<void> {
|
||||
// fetch the remote environment
|
||||
let [remoteEnv, localExtensions] = await Promise.all([
|
||||
let [localExtensions, remoteEnv, remoteExtensions] = await Promise.all([
|
||||
this._webExtensionsScannerService.scanAndTranslateExtensions().then(extensions => extensions.map(parseScannedExtension)),
|
||||
this._remoteAgentService.getEnvironment(),
|
||||
this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension))
|
||||
this._remoteAgentService.scanExtensions()
|
||||
]);
|
||||
localExtensions = this._checkEnabledAndProposedAPI(localExtensions);
|
||||
remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions);
|
||||
|
||||
const remoteAgentConnection = this._remoteAgentService.getConnection();
|
||||
this._runningLocation = _determineRunningLocation(this._productService, this._configService, localExtensions, remoteExtensions, Boolean(remoteEnv && remoteAgentConnection));
|
||||
|
||||
let result: DeltaExtensionsResult;
|
||||
localExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker);
|
||||
remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote);
|
||||
|
||||
// local: only enabled and web'ish extension
|
||||
localExtensions = localExtensions!.filter(ext => this._isEnabled(ext) && canExecuteOnWeb(ext, this._productService, this._configService));
|
||||
this._checkEnableProposedApi(localExtensions);
|
||||
|
||||
if (!remoteEnv || !remoteAgentConnection) {
|
||||
result = this._registry.deltaExtensions(localExtensions, []);
|
||||
|
||||
} else {
|
||||
// remote: only enabled and none-web'ish extension
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !canExecuteOnWeb(extension, this._productService, this._configService));
|
||||
this._checkEnableProposedApi(remoteEnv.extensions);
|
||||
|
||||
// in case of overlap, the remote wins
|
||||
const isRemoteExtension = new Set<string>();
|
||||
remoteEnv.extensions.forEach(extension => isRemoteExtension.add(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
localExtensions = localExtensions.filter(extension => !isRemoteExtension.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
const result = this._registry.deltaExtensions(remoteExtensions.concat(localExtensions), []);
|
||||
if (result.removedDueToLooping.length > 0) {
|
||||
this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', ')));
|
||||
}
|
||||
|
||||
if (remoteEnv && remoteAgentConnection) {
|
||||
// save for remote extension's init data
|
||||
this._remoteInitData = {
|
||||
connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAgentConnection.remoteAuthority),
|
||||
@@ -143,16 +139,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
extensionHostLogsPath: remoteEnv.extensionHostLogsPath,
|
||||
globalStorageHome: remoteEnv.globalStorageHome,
|
||||
workspaceStorageHome: remoteEnv.workspaceStorageHome,
|
||||
extensions: remoteEnv.extensions,
|
||||
allExtensions: remoteEnv.extensions.concat(localExtensions)
|
||||
extensions: remoteExtensions,
|
||||
allExtensions: this._registry.getAllExtensionDescriptions()
|
||||
};
|
||||
|
||||
result = this._registry.deltaExtensions(remoteEnv.extensions.concat(localExtensions), []);
|
||||
}
|
||||
|
||||
if (result.removedDueToLooping.length > 0) {
|
||||
this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', ')));
|
||||
}
|
||||
this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions());
|
||||
}
|
||||
|
||||
@@ -164,4 +155,55 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
}
|
||||
}
|
||||
|
||||
const enum ExtensionRunningLocation {
|
||||
None,
|
||||
LocalWebWorker,
|
||||
Remote
|
||||
}
|
||||
|
||||
export function determineRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], allExtensionKinds: Map<string, ExtensionKind[]>, hasRemote: boolean): Map<string, ExtensionRunningLocation> {
|
||||
const localExtensionsSet = new Set<string>();
|
||||
localExtensions.forEach(ext => localExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
const remoteExtensionsSet = new Set<string>();
|
||||
remoteExtensions.forEach(ext => remoteExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
const pickRunningLocation = (extension: IExtensionDescription): ExtensionRunningLocation => {
|
||||
const isInstalledLocally = localExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier));
|
||||
const isInstalledRemotely = remoteExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier));
|
||||
const extensionKinds = allExtensionKinds.get(ExtensionIdentifier.toKey(extension.identifier)) || [];
|
||||
for (const extensionKind of extensionKinds) {
|
||||
if (extensionKind === 'ui' && isInstalledRemotely) {
|
||||
// ui extensions run remotely if possible
|
||||
return ExtensionRunningLocation.Remote;
|
||||
}
|
||||
if (extensionKind === 'workspace' && isInstalledRemotely) {
|
||||
// workspace extensions run remotely if possible
|
||||
return ExtensionRunningLocation.Remote;
|
||||
}
|
||||
if (extensionKind === 'web' && isInstalledLocally) {
|
||||
// web worker extensions run in the local web worker if possible
|
||||
return ExtensionRunningLocation.LocalWebWorker;
|
||||
}
|
||||
}
|
||||
return ExtensionRunningLocation.None;
|
||||
};
|
||||
|
||||
const runningLocation = new Map<string, ExtensionRunningLocation>();
|
||||
localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
|
||||
remoteExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
|
||||
return runningLocation;
|
||||
}
|
||||
|
||||
function _determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean): Map<string, ExtensionRunningLocation> {
|
||||
const allExtensionKinds = new Map<string, ExtensionKind[]>();
|
||||
localExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService)));
|
||||
remoteExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService)));
|
||||
return determineRunningLocation(localExtensions, remoteExtensions, allExtensionKinds, hasRemote);
|
||||
}
|
||||
|
||||
function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map<string, ExtensionRunningLocation>, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] {
|
||||
return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation);
|
||||
}
|
||||
|
||||
registerSingleton(IExtensionService, ExtensionService);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
||||
@@ -16,6 +16,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
@@ -24,6 +25,11 @@ import { joinPath } from 'vs/base/common/resources';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
|
||||
import { localize } from 'vs/nls';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { canceled, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { WEB_WORKER_IFRAME } from 'vs/workbench/services/extensions/common/webWorkerIframe';
|
||||
|
||||
const WRAP_IN_IFRAME = true;
|
||||
|
||||
export interface IWebWorkerExtensionHostInitData {
|
||||
readonly autoStart: boolean;
|
||||
@@ -34,17 +40,17 @@ export interface IWebWorkerExtensionHostDataProvider {
|
||||
getInitData(): Promise<IWebWorkerExtensionHostInitData>;
|
||||
}
|
||||
|
||||
export class WebWorkerExtensionHost implements IExtensionHost {
|
||||
export class WebWorkerExtensionHost extends Disposable implements IExtensionHost {
|
||||
|
||||
public readonly kind = ExtensionHostKind.LocalWebWorker;
|
||||
public readonly remoteAuthority = null;
|
||||
|
||||
private _toDispose = new DisposableStore();
|
||||
private _isTerminating: boolean = false;
|
||||
private _protocol?: IMessagePassingProtocol;
|
||||
private readonly _onDidExit = this._register(new Emitter<[number, string | null]>());
|
||||
public readonly onExit: Event<[number, string | null]> = this._onDidExit.event;
|
||||
|
||||
private readonly _onDidExit = new Emitter<[number, string | null]>();
|
||||
readonly onExit: Event<[number, string | null]> = this._onDidExit.event;
|
||||
private _isTerminating: boolean;
|
||||
private _protocolPromise: Promise<IMessagePassingProtocol> | null;
|
||||
private _protocol: IMessagePassingProtocol | null;
|
||||
|
||||
private readonly _extensionHostLogsLocation: URI;
|
||||
private readonly _extensionHostLogFile: URI;
|
||||
@@ -58,76 +64,168 @@ export class WebWorkerExtensionHost implements IExtensionHost {
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
this._isTerminating = false;
|
||||
this._protocolPromise = null;
|
||||
this._protocol = null;
|
||||
this._extensionHostLogsLocation = URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme });
|
||||
this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`);
|
||||
}
|
||||
|
||||
async start(): Promise<IMessagePassingProtocol> {
|
||||
|
||||
if (!this._protocol) {
|
||||
|
||||
const emitter = new Emitter<VSBuffer>();
|
||||
|
||||
const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost');
|
||||
const worker = new Worker(url, { name: 'WorkerExtensionHost' });
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
const { data } = event;
|
||||
if (!(data instanceof ArrayBuffer)) {
|
||||
console.warn('UNKNOWN data received', data);
|
||||
this._onDidExit.fire([77, 'UNKNOWN data received']);
|
||||
return;
|
||||
}
|
||||
|
||||
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
|
||||
};
|
||||
|
||||
worker.onerror = (event) => {
|
||||
console.error(event.message, event.error);
|
||||
this._onDidExit.fire([81, event.message || event.error]);
|
||||
};
|
||||
|
||||
// keep for cleanup
|
||||
this._toDispose.add(emitter);
|
||||
this._toDispose.add(toDisposable(() => worker.terminate()));
|
||||
|
||||
const protocol: IMessagePassingProtocol = {
|
||||
onMessage: emitter.event,
|
||||
send: vsbuf => {
|
||||
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);
|
||||
worker.postMessage(data, [data]);
|
||||
}
|
||||
};
|
||||
|
||||
// extension host handshake happens below
|
||||
// (1) <== wait for: Ready
|
||||
// (2) ==> send: init data
|
||||
// (3) <== wait for: Initialized
|
||||
|
||||
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready)));
|
||||
protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData())));
|
||||
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized)));
|
||||
|
||||
// Register log channel for web worker exthost log
|
||||
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true });
|
||||
|
||||
this._protocol = protocol;
|
||||
public async start(): Promise<IMessagePassingProtocol> {
|
||||
if (!this._protocolPromise) {
|
||||
if (WRAP_IN_IFRAME && platform.isWeb) {
|
||||
this._protocolPromise = this._startInsideIframe();
|
||||
} else {
|
||||
this._protocolPromise = this._startOutsideIframe();
|
||||
}
|
||||
this._protocolPromise.then(protocol => this._protocol = protocol);
|
||||
}
|
||||
return this._protocol;
|
||||
|
||||
return this._protocolPromise;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (!this._protocol) {
|
||||
this._toDispose.dispose();
|
||||
return;
|
||||
private _startInsideIframe(): Promise<IMessagePassingProtocol> {
|
||||
const emitter = this._register(new Emitter<VSBuffer>());
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('class', 'web-worker-ext-host-iframe');
|
||||
iframe.setAttribute('sandbox', 'allow-scripts');
|
||||
iframe.style.display = 'none';
|
||||
|
||||
const vscodeWebWorkerExtHostId = generateUuid();
|
||||
const workerUrl = require.toUrl('../worker/extensionHostWorkerMain.js');
|
||||
const workerSrc = getWorkerBootstrapUrl(workerUrl, 'WorkerExtensionHost', true);
|
||||
const escapeAttribute = (value: string): string => {
|
||||
return value.replace(/"/g, '"');
|
||||
};
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-eval' '${WEB_WORKER_IFRAME.sha}' *; worker-src data:; connect-src *" />
|
||||
<meta id="vscode-worker-src" data-value="${escapeAttribute(workerSrc)}" />
|
||||
<meta id="vscode-web-worker-ext-host-id" data-value="${escapeAttribute(vscodeWebWorkerExtHostId)}" />
|
||||
</head>
|
||||
<body>
|
||||
<script>${WEB_WORKER_IFRAME.js}</script>
|
||||
</body>
|
||||
</html>`;
|
||||
const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
|
||||
iframe.setAttribute('src', iframeContent);
|
||||
|
||||
this._register(dom.addDisposableListener(window, 'message', (event) => {
|
||||
if (event.source !== iframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {
|
||||
return;
|
||||
}
|
||||
if (event.data.error) {
|
||||
const { name, message, stack } = event.data.error;
|
||||
const err = new Error();
|
||||
err.message = message;
|
||||
err.name = name;
|
||||
err.stack = stack;
|
||||
onUnexpectedError(err);
|
||||
this._onDidExit.fire([18, err.message]);
|
||||
return;
|
||||
}
|
||||
const { data } = event.data;
|
||||
if (!(data instanceof ArrayBuffer)) {
|
||||
console.warn('UNKNOWN data received', data);
|
||||
this._onDidExit.fire([77, 'UNKNOWN data received']);
|
||||
return;
|
||||
}
|
||||
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
|
||||
}));
|
||||
|
||||
const protocol: IMessagePassingProtocol = {
|
||||
onMessage: emitter.event,
|
||||
send: vsbuf => {
|
||||
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);
|
||||
iframe.contentWindow!.postMessage({
|
||||
vscodeWebWorkerExtHostId,
|
||||
data: data
|
||||
}, '*', [data]);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
this._register(toDisposable(() => iframe.remove()));
|
||||
|
||||
return this._performHandshake(protocol);
|
||||
}
|
||||
|
||||
private _startOutsideIframe(): Promise<IMessagePassingProtocol> {
|
||||
const emitter = new Emitter<VSBuffer>();
|
||||
|
||||
const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost');
|
||||
const worker = new Worker(url, { name: 'WorkerExtensionHost' });
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
const { data } = event;
|
||||
if (!(data instanceof ArrayBuffer)) {
|
||||
console.warn('UNKNOWN data received', data);
|
||||
this._onDidExit.fire([77, 'UNKNOWN data received']);
|
||||
return;
|
||||
}
|
||||
|
||||
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
|
||||
};
|
||||
|
||||
worker.onerror = (event) => {
|
||||
console.error(event.message, event.error);
|
||||
this._onDidExit.fire([81, event.message || event.error]);
|
||||
};
|
||||
|
||||
// keep for cleanup
|
||||
this._register(emitter);
|
||||
this._register(toDisposable(() => worker.terminate()));
|
||||
|
||||
const protocol: IMessagePassingProtocol = {
|
||||
onMessage: emitter.event,
|
||||
send: vsbuf => {
|
||||
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);
|
||||
worker.postMessage(data, [data]);
|
||||
}
|
||||
};
|
||||
|
||||
return this._performHandshake(protocol);
|
||||
}
|
||||
|
||||
private async _performHandshake(protocol: IMessagePassingProtocol): Promise<IMessagePassingProtocol> {
|
||||
// extension host handshake happens below
|
||||
// (1) <== wait for: Ready
|
||||
// (2) ==> send: init data
|
||||
// (3) <== wait for: Initialized
|
||||
|
||||
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready)));
|
||||
if (this._isTerminating) {
|
||||
throw canceled();
|
||||
}
|
||||
protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData())));
|
||||
if (this._isTerminating) {
|
||||
throw canceled();
|
||||
}
|
||||
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized)));
|
||||
if (this._isTerminating) {
|
||||
throw canceled();
|
||||
}
|
||||
|
||||
// Register log channel for web worker exthost log
|
||||
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true });
|
||||
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._isTerminating) {
|
||||
return;
|
||||
}
|
||||
this._isTerminating = true;
|
||||
this._protocol.send(createMessageOfType(MessageType.Terminate));
|
||||
setTimeout(() => this._toDispose.dispose(), 10 * 1000);
|
||||
if (this._protocol) {
|
||||
this._protocol.send(createMessageOfType(MessageType.Terminate));
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
getInspectPort(): number | undefined {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensi
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
|
||||
import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol';
|
||||
import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager';
|
||||
import { ExtensionIdentifier, IExtensionDescription, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, IExtensionDescription, ExtensionType, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
@@ -29,7 +29,7 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export function parseScannedExtension(extension: IScannedExtension): IExtensionDescription {
|
||||
export function parseScannedExtension(extension: ITranslatedScannedExtension): IExtensionDescription {
|
||||
return {
|
||||
identifier: new ExtensionIdentifier(`${extension.packageJSON.publisher}.${extension.packageJSON.name}`),
|
||||
isBuiltin: extension.type === ExtensionType.System,
|
||||
|
||||
@@ -27,7 +27,6 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens
|
||||
// Enable to see detailed message communication between window and extension host
|
||||
const LOG_EXTENSION_HOST_COMMUNICATION = false;
|
||||
const LOG_USE_COLORS = true;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export class ExtensionHostManager extends Disposable {
|
||||
|
||||
@@ -38,9 +37,9 @@ export class ExtensionHostManager extends Disposable {
|
||||
public readonly onDidChangeResponsiveState: Event<ResponsiveState> = this._onDidChangeResponsiveState.event;
|
||||
|
||||
/**
|
||||
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
|
||||
* A map of already requested activation events to speed things up if the same activation event is triggered multiple times.
|
||||
*/
|
||||
private readonly _finishedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private readonly _cachedActivationEvents: Map<string, Promise<void>>;
|
||||
private _rpcProtocol: RPCProtocol | null;
|
||||
private readonly _customers: IDisposable[];
|
||||
private readonly _extensionHost: IExtensionHost;
|
||||
@@ -57,7 +56,7 @@ export class ExtensionHostManager extends Disposable {
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
) {
|
||||
super();
|
||||
this._finishedActivateEvents = Object.create(null);
|
||||
this._cachedActivationEvents = new Map<string, Promise<void>>();
|
||||
this._rpcProtocol = null;
|
||||
this._customers = [];
|
||||
|
||||
@@ -221,19 +220,23 @@ export class ExtensionHostManager extends Disposable {
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._finishedActivateEvents[activationEvent] || !this._proxy) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
if (!this._cachedActivationEvents.has(activationEvent)) {
|
||||
this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent));
|
||||
}
|
||||
return this._proxy.then((proxy) => {
|
||||
if (!proxy) {
|
||||
// this case is already covered above and logged.
|
||||
// i.e. the extension host could not be started
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return proxy.value.$activateByEvent(activationEvent);
|
||||
}).then(() => {
|
||||
this._finishedActivateEvents[activationEvent] = true;
|
||||
});
|
||||
return this._cachedActivationEvents.get(activationEvent)!;
|
||||
}
|
||||
|
||||
private async _activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (!this._proxy) {
|
||||
return;
|
||||
}
|
||||
const proxy = await this._proxy;
|
||||
if (!proxy) {
|
||||
// this case is already covered above and logged.
|
||||
// i.e. the extension host could not be started
|
||||
return;
|
||||
}
|
||||
return proxy.value.$activateByEvent(activationEvent);
|
||||
}
|
||||
|
||||
public async getInspectPort(tryEnableInspector: boolean): Promise<number> {
|
||||
|
||||
@@ -150,11 +150,13 @@ const extensionKindSchema: IJSONSchema = {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'ui',
|
||||
'workspace'
|
||||
'workspace',
|
||||
'web'
|
||||
],
|
||||
enumDescriptions: [
|
||||
nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."),
|
||||
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.")
|
||||
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote."),
|
||||
nls.localize('web', "Web worker extension kind. Such an extension can execute in a web worker extension host.")
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -59,18 +59,29 @@ export function getExtensionKind(manifest: IExtensionManifest, productService: I
|
||||
return toArray(result);
|
||||
}
|
||||
|
||||
return deduceExtensionKind(manifest);
|
||||
}
|
||||
|
||||
export function deduceExtensionKind(manifest: IExtensionManifest): ExtensionKind[] {
|
||||
// Not an UI extension if it has main
|
||||
if (manifest.main) {
|
||||
if (manifest.browser) {
|
||||
return ['workspace', 'web'];
|
||||
}
|
||||
return ['workspace'];
|
||||
}
|
||||
|
||||
// Not an UI extension if it has dependencies or an extension pack
|
||||
if (manifest.browser) {
|
||||
return ['web'];
|
||||
}
|
||||
|
||||
// Not an UI nor web extension if it has dependencies or an extension pack
|
||||
if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) {
|
||||
return ['workspace'];
|
||||
}
|
||||
|
||||
if (manifest.contributes) {
|
||||
// Not an UI extension if it has no ui contributions
|
||||
// Not an UI nor web extension if it has no ui contributions
|
||||
for (const contribution of Object.keys(manifest.contributes)) {
|
||||
if (!isUIExtensionPoint(contribution)) {
|
||||
return ['workspace'];
|
||||
@@ -78,7 +89,7 @@ export function getExtensionKind(manifest: IExtensionManifest, productService: I
|
||||
}
|
||||
}
|
||||
|
||||
return ['ui', 'workspace'];
|
||||
return ['ui', 'workspace', 'web'];
|
||||
}
|
||||
|
||||
let _uiExtensionPoints: Set<string> | null = null;
|
||||
|
||||
@@ -96,7 +96,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost {
|
||||
}
|
||||
},
|
||||
signService: this._signService,
|
||||
logService: this._logService
|
||||
logService: this._logService,
|
||||
ipcLogger: null
|
||||
};
|
||||
return this.remoteAuthorityResolverService.resolveAuthority(this._initDataProvider.remoteAuthority).then((resolverResult) => {
|
||||
|
||||
@@ -204,8 +205,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost {
|
||||
const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]);
|
||||
|
||||
// Collect all identifiers for extension ids which can be considered "resolved"
|
||||
const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main).map(extension => extension.identifier);
|
||||
const hostExtensions = remoteInitData.allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier);
|
||||
const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier);
|
||||
const hostExtensions = remoteInitData.allExtensions.filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
return {
|
||||
commit: this._productService.commit,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const WEB_WORKER_IFRAME = {
|
||||
sha: 'sha256-rSINb5Ths99Zj4Ml59jEdHS4WbO+H5Iw+oyRmyi2MLw=',
|
||||
js: `
|
||||
(function() {
|
||||
const workerSrc = document.getElementById('vscode-worker-src').getAttribute('data-value');
|
||||
const worker = new Worker(workerSrc, { name: 'WorkerExtensionHost' });
|
||||
const vscodeWebWorkerExtHostId = document.getElementById('vscode-web-worker-ext-host-id').getAttribute('data-value');
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
const { data } = event;
|
||||
if (!(data instanceof ArrayBuffer)) {
|
||||
console.warn('Unknown data received', data);
|
||||
window.parent.postMessage({
|
||||
vscodeWebWorkerExtHostId,
|
||||
error: {
|
||||
name: 'Error',
|
||||
message: 'Unknown data received',
|
||||
stack: []
|
||||
}
|
||||
}, '*');
|
||||
return;
|
||||
}
|
||||
window.parent.postMessage({
|
||||
vscodeWebWorkerExtHostId,
|
||||
data: data
|
||||
}, '*', [data]);
|
||||
};
|
||||
|
||||
worker.onerror = (event) => {
|
||||
console.error(event.message, event.error);
|
||||
window.parent.postMessage({
|
||||
vscodeWebWorkerExtHostId,
|
||||
error: {
|
||||
name: event.error ? event.error.name : '',
|
||||
message: event.error ? event.error.message : '',
|
||||
stack: event.error ? event.error.stack : []
|
||||
}
|
||||
}, '*');
|
||||
};
|
||||
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.source !== window.parent) {
|
||||
return;
|
||||
}
|
||||
if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {
|
||||
return;
|
||||
}
|
||||
worker.postMessage(event.data.data, [event.data.data]);
|
||||
}, false);
|
||||
})();
|
||||
`
|
||||
};
|
||||
@@ -24,7 +24,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost, webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager';
|
||||
import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription, ExtensionKind } from 'vs/platform/extensions/common/extensions';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
@@ -377,7 +377,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
private async _scanAllLocalExtensions(): Promise<IExtensionDescription[]> {
|
||||
return flatten(await Promise.all([
|
||||
this._extensionScanner.scannedExtensions,
|
||||
this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension))
|
||||
this._webExtensionsScannerService.scanAndTranslateExtensions().then(extensions => extensions.map(parseScannedExtension))
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
getInitData: async () => {
|
||||
if (isInitialStart) {
|
||||
const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions());
|
||||
const runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, [], false, this._enableLocalWebWorker);
|
||||
const runningLocation = _determineRunningLocation(this._productService, this._configurationService, localExtensions, [], false, this._enableLocalWebWorker);
|
||||
const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation);
|
||||
return {
|
||||
autoStart: false,
|
||||
@@ -503,8 +503,9 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
const remoteAuthority = this._environmentService.configuration.remoteAuthority;
|
||||
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!;
|
||||
|
||||
let localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions());
|
||||
const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions());
|
||||
let remoteEnv: IRemoteAgentEnvironment | null = null;
|
||||
let remoteExtensions: IExtensionDescription[] = [];
|
||||
|
||||
if (remoteAuthority) {
|
||||
let resolverResult: ResolverResult;
|
||||
@@ -543,7 +544,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
}
|
||||
|
||||
// fetch the remote environment
|
||||
remoteEnv = await this._remoteAgentService.getEnvironment();
|
||||
[remoteEnv, remoteExtensions] = await Promise.all([
|
||||
this._remoteAgentService.getEnvironment(),
|
||||
this._remoteAgentService.scanExtensions()
|
||||
]);
|
||||
remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions);
|
||||
|
||||
if (!remoteEnv) {
|
||||
this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") });
|
||||
@@ -553,14 +558,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
}
|
||||
}
|
||||
|
||||
await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv);
|
||||
await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv, remoteExtensions);
|
||||
}
|
||||
|
||||
private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null): Promise<void> {
|
||||
private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null, remoteExtensions: IExtensionDescription[] = []): Promise<void> {
|
||||
|
||||
let remoteExtensions = remoteEnv ? this._checkEnabledAndProposedAPI(remoteEnv.extensions) : [];
|
||||
|
||||
this._runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, remoteExtensions, Boolean(remoteAuthority), this._enableLocalWebWorker);
|
||||
this._runningLocation = _determineRunningLocation(this._productService, this._configurationService, localExtensions, remoteExtensions, Boolean(remoteAuthority), this._enableLocalWebWorker);
|
||||
|
||||
// remove non-UI extensions from the local extensions
|
||||
const localProcessExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalProcess);
|
||||
@@ -686,7 +689,7 @@ const enum ExtensionRunningLocation {
|
||||
Remote
|
||||
}
|
||||
|
||||
function determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean, hasLocalWebWorker: boolean): Map<string, ExtensionRunningLocation> {
|
||||
export function determineRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], allExtensionKinds: Map<string, ExtensionKind[]>, hasRemote: boolean, hasLocalWebWorker: boolean): Map<string, ExtensionRunningLocation> {
|
||||
const localExtensionsSet = new Set<string>();
|
||||
localExtensions.forEach(ext => localExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
@@ -696,7 +699,8 @@ function determineRunningLocation(productService: IProductService, configuration
|
||||
const pickRunningLocation = (extension: IExtensionDescription): ExtensionRunningLocation => {
|
||||
const isInstalledLocally = localExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier));
|
||||
const isInstalledRemotely = remoteExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier));
|
||||
for (const extensionKind of getExtensionKind(extension, productService, configurationService)) {
|
||||
const extensionKinds = allExtensionKinds.get(ExtensionIdentifier.toKey(extension.identifier)) || [];
|
||||
for (const extensionKind of extensionKinds) {
|
||||
if (extensionKind === 'ui' && isInstalledLocally) {
|
||||
// ui extensions run locally if possible
|
||||
return ExtensionRunningLocation.LocalProcess;
|
||||
@@ -711,10 +715,6 @@ function determineRunningLocation(productService: IProductService, configuration
|
||||
}
|
||||
if (extensionKind === 'web' && isInstalledLocally && hasLocalWebWorker) {
|
||||
// web worker extensions run in the local web worker if possible
|
||||
if (typeof extension.browser !== 'undefined') {
|
||||
// The "browser" field determines the entry point
|
||||
(<any>extension).main = extension.browser;
|
||||
}
|
||||
return ExtensionRunningLocation.LocalWebWorker;
|
||||
}
|
||||
}
|
||||
@@ -727,6 +727,13 @@ function determineRunningLocation(productService: IProductService, configuration
|
||||
return runningLocation;
|
||||
}
|
||||
|
||||
function _determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean, hasLocalWebWorker: boolean): Map<string, ExtensionRunningLocation> {
|
||||
const allExtensionKinds = new Map<string, ExtensionKind[]>();
|
||||
localExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService)));
|
||||
remoteExtensions.forEach(ext => allExtensionKinds.set(ExtensionIdentifier.toKey(ext.identifier), getExtensionKind(ext, productService, configurationService)));
|
||||
return determineRunningLocation(localExtensions, remoteExtensions, allExtensionKinds, hasRemote, hasLocalWebWorker);
|
||||
}
|
||||
|
||||
function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map<string, ExtensionRunningLocation>, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] {
|
||||
return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ import { joinPath } from 'vs/base/common/resources';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
|
||||
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
|
||||
import { isUUID } from 'vs/base/common/uuid';
|
||||
|
||||
export interface ILocalProcessExtensionHostInitData {
|
||||
readonly autoStart: boolean;
|
||||
@@ -183,23 +182,18 @@ export class LocalProcessExtensionHost implements IExtensionHost {
|
||||
opts.execArgv = ['--inspect-port=0'];
|
||||
}
|
||||
|
||||
// On linux crash reporter needs to be started on child node processes explicitly
|
||||
if (platform.isLinux) {
|
||||
const crashReporterStartOptions: CrashReporterStartOptions = {
|
||||
// Enable the crash reporter depending on environment for local reporting
|
||||
const crashesDirectory = this._environmentService.crashReporterDirectory;
|
||||
if (crashesDirectory) {
|
||||
const crashReporterOptions: CrashReporterStartOptions = {
|
||||
companyName: this._productService.crashReporter?.companyName || 'Microsoft',
|
||||
productName: this._productService.crashReporter?.productName || this._productService.nameShort,
|
||||
submitURL: '',
|
||||
uploadToServer: false
|
||||
uploadToServer: false,
|
||||
crashesDirectory
|
||||
};
|
||||
const crashReporterId = this._environmentService.crashReporterId; // crashReporterId is set by the main process only when crash reporting is enabled by the user.
|
||||
const appcenter = this._productService.appCenter;
|
||||
const uploadCrashesToServer = !this._environmentService.crashReporterDirectory; // only upload unless --crash-reporter-directory is provided
|
||||
if (uploadCrashesToServer && appcenter && crashReporterId && isUUID(crashReporterId)) {
|
||||
const submitURL = appcenter[`linux-x64`];
|
||||
crashReporterStartOptions.submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId);
|
||||
crashReporterStartOptions.uploadToServer = true;
|
||||
}
|
||||
opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterStartOptions);
|
||||
|
||||
opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions);
|
||||
}
|
||||
|
||||
// Run Extension Host as fork of current process
|
||||
|
||||
@@ -393,9 +393,8 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
notices.push(nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main'));
|
||||
return false;
|
||||
} else {
|
||||
let normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.main);
|
||||
|
||||
if (normalizedAbsolutePath.indexOf(extensionFolderPath)) {
|
||||
const normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.main);
|
||||
if (!normalizedAbsolutePath.startsWith(extensionFolderPath)) {
|
||||
notices.push(nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", normalizedAbsolutePath, extensionFolderPath));
|
||||
// not a failure case
|
||||
}
|
||||
@@ -405,6 +404,22 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (typeof extensionDescription.browser !== 'undefined') {
|
||||
if (typeof extensionDescription.browser !== 'string') {
|
||||
notices.push(nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser'));
|
||||
return false;
|
||||
} else {
|
||||
const normalizedAbsolutePath = path.join(extensionFolderPath, extensionDescription.browser);
|
||||
if (!normalizedAbsolutePath.startsWith(extensionFolderPath)) {
|
||||
notices.push(nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", normalizedAbsolutePath, extensionFolderPath));
|
||||
// not a failure case
|
||||
}
|
||||
}
|
||||
if (typeof extensionDescription.activationEvents === 'undefined') {
|
||||
notices.push(nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { deduceExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IExtensionManifest, ExtensionKind } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
suite('ExtensionKind', () => {
|
||||
|
||||
function check(manifest: Partial<IExtensionManifest>, expected: ExtensionKind[]): void {
|
||||
assert.deepEqual(deduceExtensionKind(<IExtensionManifest>manifest), expected);
|
||||
}
|
||||
|
||||
test('declarative with extension dependencies => workspace', () => {
|
||||
check({ extensionDependencies: ['ext1'] }, ['workspace']);
|
||||
});
|
||||
|
||||
test('declarative extension pack => workspace', () => {
|
||||
check({ extensionPack: ['ext1', 'ext2'] }, ['workspace']);
|
||||
});
|
||||
|
||||
test('declarative with unknown contribution point => workspace', () => {
|
||||
check({ contributes: <any>{ 'unknownPoint': { something: true } } }, ['workspace']);
|
||||
});
|
||||
|
||||
test('simple declarative => ui, workspace, web', () => {
|
||||
check({}, ['ui', 'workspace', 'web']);
|
||||
});
|
||||
|
||||
test('only browser => web', () => {
|
||||
check({ browser: 'main.browser.js' }, ['web']);
|
||||
});
|
||||
|
||||
test('only main => workspace', () => {
|
||||
check({ main: 'main.js' }, ['workspace']);
|
||||
});
|
||||
|
||||
test('main and browser => workspace, web', () => {
|
||||
check({ main: 'main.js', browser: 'main.browser.js' }, ['workspace', 'web']);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { isMessageOfType, MessageType, createMessageOfType } from 'vs/workbench/
|
||||
import { IInitData } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ExtensionHostMain } from 'vs/workbench/services/extensions/common/extensionHostMain';
|
||||
import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService';
|
||||
import * as path from 'vs/base/common/path';
|
||||
|
||||
import 'vs/workbench/api/common/extHost.common.services';
|
||||
import 'vs/workbench/api/worker/extHost.worker.services';
|
||||
@@ -35,6 +36,17 @@ self.postMessage = () => console.trace(`'postMessage' has been blocked`);
|
||||
const nativeAddEventLister = addEventListener.bind(self);
|
||||
self.addEventLister = () => console.trace(`'addEventListener' has been blocked`);
|
||||
|
||||
if (location.protocol === 'data:') {
|
||||
// make sure new Worker(...) always uses data:
|
||||
const _Worker = Worker;
|
||||
Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) {
|
||||
const js = `importScripts('${stringUrl}');`;
|
||||
options = options || {};
|
||||
options.name = options.name || path.basename(stringUrl.toString());
|
||||
return new _Worker(`data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`, options);
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion ---
|
||||
|
||||
const hostUtil = new class implements IHostUtils {
|
||||
|
||||
@@ -51,6 +51,10 @@ const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoin
|
||||
type: 'string',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.separator', "Separator to be used in the uri label display. '/' or '\' as an example.")
|
||||
},
|
||||
stripPathStartingSeparator: {
|
||||
type: 'boolean',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.stripPathStartingSeparator', "Controls whether `${path}` substitutions should have starting separator characters stripped.")
|
||||
},
|
||||
tildify: {
|
||||
type: 'boolean',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.tildify', "Controls if the start of the uri label should be tildified when possible.")
|
||||
@@ -244,7 +248,10 @@ export class LabelService extends Disposable implements ILabelService {
|
||||
switch (token) {
|
||||
case 'scheme': return resource.scheme;
|
||||
case 'authority': return resource.authority;
|
||||
case 'path': return resource.path;
|
||||
case 'path':
|
||||
return formatting.stripPathStartingSeparator
|
||||
? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0)
|
||||
: resource.path;
|
||||
default: {
|
||||
if (qsToken === 'query') {
|
||||
const { query } = resource;
|
||||
|
||||
@@ -226,6 +226,26 @@ suite('multi-root worksapce', () => {
|
||||
const generated = labelService.getUriLabel(URI.file(path), { relative: true });
|
||||
assert.equal(generated, label, path);
|
||||
});
|
||||
});
|
||||
|
||||
test('stripPathStartingSeparator', () => {
|
||||
labelService.registerFormatter({
|
||||
scheme: 'file',
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: '/',
|
||||
stripPathStartingSeparator: true
|
||||
}
|
||||
});
|
||||
|
||||
const tests = {
|
||||
'folder1/src/file': 'Sources • file',
|
||||
'other/blah': 'other/blah',
|
||||
};
|
||||
|
||||
Object.entries(tests).forEach(([path, label]) => {
|
||||
const generated = labelService.getUriLabel(URI.file(path), { relative: true });
|
||||
assert.equal(generated, label, path);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,21 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { AbstractRemoteAgentService, RemoteAgentConnection } from 'vs/workbench/services/remote/common/abstractRemoteAgentService';
|
||||
import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IWebSocketFactory, BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService {
|
||||
|
||||
public readonly socketFactory: ISocketFactory;
|
||||
|
||||
private readonly _connection: IRemoteAgentConnection | null = null;
|
||||
|
||||
constructor(
|
||||
webSocketFactory: IWebSocketFactory | null | undefined,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@@ -27,16 +22,6 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR
|
||||
@ISignService signService: ISignService,
|
||||
@ILogService logService: ILogService
|
||||
) {
|
||||
super(environmentService, remoteAuthorityResolverService);
|
||||
|
||||
this.socketFactory = new BrowserSocketFactory(webSocketFactory);
|
||||
const remoteAuthority = environmentService.configuration.remoteAuthority;
|
||||
if (remoteAuthority) {
|
||||
this._connection = this._register(new RemoteAgentConnection(remoteAuthority, productService.commit, this.socketFactory, remoteAuthorityResolverService, signService, logService));
|
||||
}
|
||||
}
|
||||
|
||||
getConnection(): IRemoteAgentConnection | null {
|
||||
return this._connection;
|
||||
super(new BrowserSocketFactory(webSocketFactory), environmentService, productService, remoteAuthorityResolverService, signService, logService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IChannel, IServerChannel, getDelayedChannel, IPCLogger } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, PersistenConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
@@ -22,35 +22,62 @@ import { Emitter } from 'vs/base/common/event';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
||||
export abstract class AbstractRemoteAgentService extends Disposable {
|
||||
export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
public readonly socketFactory: ISocketFactory;
|
||||
private readonly _connection: IRemoteAgentConnection | null;
|
||||
private _environment: Promise<IRemoteAgentEnvironment | null> | null;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService protected readonly _environmentService: IEnvironmentService,
|
||||
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService
|
||||
socketFactory: ISocketFactory,
|
||||
@IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IProductService productService: IProductService,
|
||||
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||
@ISignService signService: ISignService,
|
||||
@ILogService logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this.socketFactory = socketFactory;
|
||||
if (this._environmentService.configuration.remoteAuthority) {
|
||||
this._connection = this._register(new RemoteAgentConnection(this._environmentService.configuration.remoteAuthority, productService.commit, this.socketFactory, this._remoteAuthorityResolverService, signService, logService));
|
||||
} else {
|
||||
this._connection = null;
|
||||
}
|
||||
this._environment = null;
|
||||
}
|
||||
|
||||
abstract getConnection(): IRemoteAgentConnection | null;
|
||||
getConnection(): IRemoteAgentConnection | null {
|
||||
return this._connection;
|
||||
}
|
||||
|
||||
getEnvironment(bail?: boolean): Promise<IRemoteAgentEnvironment | null> {
|
||||
getEnvironment(): Promise<IRemoteAgentEnvironment | null> {
|
||||
return this.getRawEnvironment().then(undefined, () => null);
|
||||
}
|
||||
|
||||
getRawEnvironment(): Promise<IRemoteAgentEnvironment | null> {
|
||||
if (!this._environment) {
|
||||
this._environment = this._withChannel(
|
||||
async (channel, connection) => {
|
||||
const env = await RemoteExtensionEnvironmentChannelClient.getEnvironmentData(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI);
|
||||
const env = await RemoteExtensionEnvironmentChannelClient.getEnvironmentData(channel, connection.remoteAuthority);
|
||||
this._remoteAuthorityResolverService._setAuthorityConnectionToken(connection.remoteAuthority, env.connectionToken);
|
||||
return env;
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
return bail ? this._environment : this._environment.then(undefined, () => null);
|
||||
return this._environment;
|
||||
}
|
||||
|
||||
scanExtensions(skipExtensions: ExtensionIdentifier[] = []): Promise<IExtensionDescription[]> {
|
||||
return this._withChannel(
|
||||
(channel, connection) => RemoteExtensionEnvironmentChannelClient.scanExtensions(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI, skipExtensions),
|
||||
[]
|
||||
).then(undefined, () => []);
|
||||
}
|
||||
|
||||
getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined> {
|
||||
@@ -152,7 +179,8 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon
|
||||
}
|
||||
},
|
||||
signService: this._signService,
|
||||
logService: this._logService
|
||||
logService: this._logService,
|
||||
ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null
|
||||
};
|
||||
const connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`));
|
||||
this._register(connection.onDidStateChange(e => this._onDidStateChange.fire(e)));
|
||||
@@ -167,7 +195,7 @@ class RemoteConnectionFailureNotificationContribution implements IWorkbenchContr
|
||||
@INotificationService notificationService: INotificationService,
|
||||
) {
|
||||
// Let's cover the case where connecting to fetch the remote extension info fails
|
||||
remoteAgentService.getEnvironment(true)
|
||||
remoteAgentService.getRawEnvironment()
|
||||
.then(undefined, err => {
|
||||
if (!RemoteAuthorityResolverError.isHandled(err)) {
|
||||
notificationService.error(nls.localize('connectionError', "Failed to connect to the remote extension host server (Error: {0})", err ? err.message : ''));
|
||||
|
||||
@@ -6,15 +6,20 @@
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
|
||||
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
export interface IGetEnvironmentDataArguments {
|
||||
remoteAuthority: string;
|
||||
}
|
||||
|
||||
export interface IScanExtensionsArguments {
|
||||
language: string;
|
||||
remoteAuthority: string;
|
||||
extensionDevelopmentPath: UriComponents[] | undefined;
|
||||
skipExtensions: ExtensionIdentifier[];
|
||||
}
|
||||
|
||||
export interface IRemoteAgentEnvironmentDTO {
|
||||
@@ -28,17 +33,14 @@ export interface IRemoteAgentEnvironmentDTO {
|
||||
globalStorageHome: UriComponents;
|
||||
workspaceStorageHome: UriComponents;
|
||||
userHome: UriComponents;
|
||||
extensions: IExtensionDescription[];
|
||||
os: platform.OperatingSystem;
|
||||
}
|
||||
|
||||
export class RemoteExtensionEnvironmentChannelClient {
|
||||
|
||||
static async getEnvironmentData(channel: IChannel, remoteAuthority: string, extensionDevelopmentPath?: URI[]): Promise<IRemoteAgentEnvironment> {
|
||||
static async getEnvironmentData(channel: IChannel, remoteAuthority: string): Promise<IRemoteAgentEnvironment> {
|
||||
const args: IGetEnvironmentDataArguments = {
|
||||
language: platform.language,
|
||||
remoteAuthority,
|
||||
extensionDevelopmentPath
|
||||
remoteAuthority
|
||||
};
|
||||
|
||||
const data = await channel.call<IRemoteAgentEnvironmentDTO>('getEnvironmentData', args);
|
||||
@@ -54,11 +56,24 @@ export class RemoteExtensionEnvironmentChannelClient {
|
||||
globalStorageHome: URI.revive(data.globalStorageHome),
|
||||
workspaceStorageHome: URI.revive(data.workspaceStorageHome),
|
||||
userHome: URI.revive(data.userHome),
|
||||
extensions: data.extensions.map(ext => { (<any>ext).extensionLocation = URI.revive(ext.extensionLocation); return ext; }),
|
||||
os: data.os
|
||||
};
|
||||
}
|
||||
|
||||
static async scanExtensions(channel: IChannel, remoteAuthority: string, extensionDevelopmentPath: URI[] | undefined, skipExtensions: ExtensionIdentifier[]): Promise<IExtensionDescription[]> {
|
||||
const args: IScanExtensionsArguments = {
|
||||
language: platform.language,
|
||||
remoteAuthority,
|
||||
extensionDevelopmentPath,
|
||||
skipExtensions
|
||||
};
|
||||
|
||||
const extensions = await channel.call<IExtensionDescription[]>('scanExtensions', args);
|
||||
extensions.forEach(ext => { (<any>ext).extensionLocation = URI.revive(ext.extensionLocation); });
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
static getDiagnosticInfo(channel: IChannel, options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo> {
|
||||
return channel.call<IDiagnosticInfo>('getDiagnosticInfo', options);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { PersistenConnectionEvent as PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export const RemoteExtensionLogFileName = 'remoteagent';
|
||||
|
||||
@@ -21,7 +22,18 @@ export interface IRemoteAgentService {
|
||||
readonly socketFactory: ISocketFactory;
|
||||
|
||||
getConnection(): IRemoteAgentConnection | null;
|
||||
getEnvironment(bail?: boolean): Promise<IRemoteAgentEnvironment | null>;
|
||||
/**
|
||||
* Get the remote environment. In case of an error, returns `null`.
|
||||
*/
|
||||
getEnvironment(): Promise<IRemoteAgentEnvironment | null>;
|
||||
/**
|
||||
* Get the remote environment. Can return an error.
|
||||
*/
|
||||
getRawEnvironment(): Promise<IRemoteAgentEnvironment | null>;
|
||||
/**
|
||||
* Scan remote extensions.
|
||||
*/
|
||||
scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise<IExtensionDescription[]>;
|
||||
getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined>;
|
||||
disableTelemetry(): Promise<void>;
|
||||
logTelemetry(eventName: string, data?: ITelemetryData): Promise<void>;
|
||||
|
||||
@@ -3,37 +3,23 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
|
||||
import { AbstractRemoteAgentService, RemoteAgentConnection } from 'vs/workbench/services/remote/common/abstractRemoteAgentService';
|
||||
import { AbstractRemoteAgentService } from 'vs/workbench/services/remote/common/abstractRemoteAgentService';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService {
|
||||
|
||||
public readonly socketFactory: ISocketFactory;
|
||||
|
||||
private readonly _connection: IRemoteAgentConnection | null = null;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IProductService productService: IProductService,
|
||||
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||
@ISignService signService: ISignService,
|
||||
@ILogService logService: ILogService,
|
||||
@IProductService productService: IProductService
|
||||
) {
|
||||
super(environmentService, remoteAuthorityResolverService);
|
||||
this.socketFactory = nodeSocketFactory;
|
||||
if (environmentService.configuration.remoteAuthority) {
|
||||
this._connection = this._register(new RemoteAgentConnection(environmentService.configuration.remoteAuthority, productService.commit, nodeSocketFactory, remoteAuthorityResolverService, signService, logService));
|
||||
}
|
||||
}
|
||||
|
||||
getConnection(): IRemoteAgentConnection | null {
|
||||
return this._connection;
|
||||
super(nodeSocketFactory, environmentService, productService, remoteAuthorityResolverService, signService, logService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,13 @@ export class ReplacePattern {
|
||||
*/
|
||||
getReplaceString(text: string, preserveCase?: boolean): string | null {
|
||||
this._regExp.lastIndex = 0;
|
||||
let match = this._regExp.exec(text);
|
||||
const match = this._regExp.exec(text);
|
||||
if (match) {
|
||||
if (this.hasParameters) {
|
||||
if (match[0] === text) {
|
||||
return text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
|
||||
}
|
||||
let replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
|
||||
const replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
|
||||
return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length));
|
||||
}
|
||||
return this.buildReplaceString(match, preserveCase);
|
||||
@@ -94,7 +94,7 @@ export class ReplacePattern {
|
||||
|
||||
let substrFrom = 0, result = '';
|
||||
for (let i = 0, len = replaceString.length; i < len; i++) {
|
||||
let chCode = replaceString.charCodeAt(i);
|
||||
const chCode = replaceString.charCodeAt(i);
|
||||
|
||||
if (chCode === CharCode.Backslash) {
|
||||
|
||||
@@ -106,7 +106,7 @@ export class ReplacePattern {
|
||||
break;
|
||||
}
|
||||
|
||||
let nextChCode = replaceString.charCodeAt(i);
|
||||
const nextChCode = replaceString.charCodeAt(i);
|
||||
let replaceWithCharacter: string | null = null;
|
||||
|
||||
switch (nextChCode) {
|
||||
@@ -140,7 +140,7 @@ export class ReplacePattern {
|
||||
break;
|
||||
}
|
||||
|
||||
let nextChCode = replaceString.charCodeAt(i);
|
||||
const nextChCode = replaceString.charCodeAt(i);
|
||||
let replaceWithCharacter: string | null = null;
|
||||
|
||||
switch (nextChCode) {
|
||||
|
||||
@@ -179,7 +179,7 @@ export class SearchService extends Disposable implements ISearchService {
|
||||
}
|
||||
|
||||
private async waitForProvider(queryType: QueryType, scheme: string): Promise<ISearchResultProvider> {
|
||||
let deferredMap: Map<string, DeferredPromise<ISearchResultProvider>> = queryType === QueryType.File ?
|
||||
const deferredMap: Map<string, DeferredPromise<ISearchResultProvider>> = queryType === QueryType.File ?
|
||||
this.deferredFileSearchesByScheme :
|
||||
this.deferredTextSearchesByScheme;
|
||||
|
||||
|
||||
@@ -162,12 +162,12 @@ export function rgErrorMsgForDisplay(msg: string): Maybe<SearchError> {
|
||||
}
|
||||
|
||||
export function buildRegexParseError(lines: string[]): string {
|
||||
let errorMessage: string[] = ['Regex parse error'];
|
||||
let pcre2ErrorLine = lines.filter(l => (l.startsWith('PCRE2:')));
|
||||
const errorMessage: string[] = ['Regex parse error'];
|
||||
const pcre2ErrorLine = lines.filter(l => (l.startsWith('PCRE2:')));
|
||||
if (pcre2ErrorLine.length >= 1) {
|
||||
let pcre2ErrorMessage = pcre2ErrorLine[0].replace('PCRE2:', '');
|
||||
const pcre2ErrorMessage = pcre2ErrorLine[0].replace('PCRE2:', '');
|
||||
if (pcre2ErrorMessage.indexOf(':') !== -1 && pcre2ErrorMessage.split(':').length >= 2) {
|
||||
let pcre2ActualErrorMessage = pcre2ErrorMessage.split(':')[1];
|
||||
const pcre2ActualErrorMessage = pcre2ErrorMessage.split(':')[1];
|
||||
errorMessage.push(':' + pcre2ActualErrorMessage);
|
||||
}
|
||||
}
|
||||
@@ -300,12 +300,12 @@ export class RipgrepParser extends EventEmitter {
|
||||
match.end = match.end <= 3 ? 0 : match.end - 3;
|
||||
}
|
||||
const inBetweenChars = fullTextBytes.slice(prevMatchEnd, match.start).toString().length;
|
||||
let startCol = prevMatchEndCol + inBetweenChars;
|
||||
const startCol = prevMatchEndCol + inBetweenChars;
|
||||
|
||||
const stats = getNumLinesAndLastNewlineLength(matchText);
|
||||
const startLineNumber = prevMatchEndLine;
|
||||
const endLineNumber = stats.numLines + startLineNumber;
|
||||
let endCol = stats.numLines > 0 ?
|
||||
const endCol = stats.numLines > 0 ?
|
||||
stats.lastLineLength :
|
||||
stats.lastLineLength + startCol;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ReplacePattern } from 'vs/workbench/services/search/common/replace';
|
||||
suite('Replace Pattern test', () => {
|
||||
|
||||
test('parse replace string', () => {
|
||||
let testParse = (input: string, expected: string, expectedHasParameters: boolean) => {
|
||||
const testParse = (input: string, expected: string, expectedHasParameters: boolean) => {
|
||||
let actual = new ReplacePattern(input, { pattern: 'somepattern', isRegExp: true });
|
||||
assert.equal(expected, actual.pattern);
|
||||
assert.equal(expectedHasParameters, actual.hasParameters);
|
||||
|
||||
@@ -387,7 +387,7 @@ suite('TextSearch-integration', function () {
|
||||
throw new Error('expected fail');
|
||||
}, err => {
|
||||
const searchError = deserializeSearchError(err);
|
||||
let regexParseErrorForUnclosedParenthesis = 'Regex parse error: unmatched closing parenthesis';
|
||||
const regexParseErrorForUnclosedParenthesis = 'Regex parse error: unmatched closing parenthesis';
|
||||
assert.equal(searchError.message, regexParseErrorForUnclosedParenthesis);
|
||||
assert.equal(searchError.code, SearchErrorCode.regexParseError);
|
||||
});
|
||||
@@ -404,7 +404,7 @@ suite('TextSearch-integration', function () {
|
||||
throw new Error('expected fail');
|
||||
}, err => {
|
||||
const searchError = deserializeSearchError(err);
|
||||
let regexParseErrorForLookAround = 'Regex parse error: lookbehind assertion is not fixed length';
|
||||
const regexParseErrorForLookAround = 'Regex parse error: lookbehind assertion is not fixed length';
|
||||
assert.equal(searchError.message, regexParseErrorForLookAround);
|
||||
assert.equal(searchError.code, SearchErrorCode.regexParseError);
|
||||
});
|
||||
|
||||
@@ -3,21 +3,20 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IAuthenticationProvider, isAuthenticationProvider, IUserDataAutoSyncService, SyncResource, IResourcePreview, ISyncResourcePreview, Change, IManualSyncTask, IUserDataSyncStoreManagementService, UserDataSyncStoreType, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel, IUserDataSyncPreview, IUserDataSyncResource, CONTEXT_ENABLE_SYNC_MERGES_VIEW, SYNC_MERGES_VIEW_ID, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_VIEW_CONTAINER_ID, SYNC_TITLE } from 'vs/workbench/services/userDataSync/common/userDataSync';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { flatten, equals } from 'vs/base/common/arrays';
|
||||
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import { getAuthenticationProviderActivationEvent, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
|
||||
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { localize } from 'vs/nls';
|
||||
@@ -30,6 +29,9 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { isNative } from 'vs/base/common/platform';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
|
||||
type UserAccountClassification = {
|
||||
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
|
||||
@@ -64,7 +66,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession';
|
||||
private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference';
|
||||
|
||||
readonly authenticationProviders: IAuthenticationProvider[];
|
||||
private _authenticationProviders: IAuthenticationProvider[] = [];
|
||||
get enabled() { return this._authenticationProviders.length > 0; }
|
||||
|
||||
private availableAuthenticationProviders: IAuthenticationProvider[] = [];
|
||||
get authenticationProviders() { return this.availableAuthenticationProviders; }
|
||||
|
||||
private _accountStatus: AccountStatus = AccountStatus.Uninitialized;
|
||||
get accountStatus(): AccountStatus { return this._accountStatus; }
|
||||
@@ -93,9 +99,8 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
@IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IProductService productService: IProductService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IProgressService private readonly progressService: IProgressService,
|
||||
@@ -103,37 +108,50 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
|
||||
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
) {
|
||||
super();
|
||||
this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || [];
|
||||
this._authenticationProviders = this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || [];
|
||||
this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
|
||||
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
|
||||
this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService);
|
||||
this.activityViewsEnablementContext = CONTEXT_ENABLE_ACTIVITY_VIEWS.bindTo(contextKeyService);
|
||||
this.mergesViewEnablementContext = CONTEXT_ENABLE_SYNC_MERGES_VIEW.bindTo(contextKeyService);
|
||||
|
||||
if (this.authenticationProviders.length) {
|
||||
|
||||
if (this._authenticationProviders.length) {
|
||||
this.syncStatusContext.set(this.userDataSyncService.status);
|
||||
this._register(userDataSyncService.onDidChangeStatus(status => this.syncStatusContext.set(status)));
|
||||
this.syncEnablementContext.set(userDataAutoSyncService.isEnabled());
|
||||
this._register(userDataAutoSyncService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled)));
|
||||
|
||||
extensionService.whenInstalledExtensionsRegistered().then(() => {
|
||||
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
|
||||
this.initialize();
|
||||
} else {
|
||||
const disposable = this.authenticationService.onDidRegisterAuthenticationProvider(() => {
|
||||
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
|
||||
disposable.dispose();
|
||||
this.initialize();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.waitAndInitialize();
|
||||
}
|
||||
}
|
||||
|
||||
private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
|
||||
return this._authenticationProviders.some(({ id }) => id === authenticationProviderId);
|
||||
}
|
||||
|
||||
private async waitAndInitialize(): Promise<void> {
|
||||
await this.extensionService.whenInstalledExtensionsRegistered();
|
||||
|
||||
/* activate unregistered providers */
|
||||
const unregisteredProviders = this._authenticationProviders.filter(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id));
|
||||
if (unregisteredProviders.length) {
|
||||
await Promise.all(unregisteredProviders.map(({ id }) => this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id))));
|
||||
}
|
||||
|
||||
/* wait until all providers are availabe */
|
||||
if (this._authenticationProviders.some(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id))) {
|
||||
await Event.toPromise(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, () => this._authenticationProviders.every(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id))));
|
||||
}
|
||||
|
||||
/* initialize */
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) {
|
||||
this.currentSessionId = this.environmentService.options.authenticationSessionId;
|
||||
@@ -158,8 +176,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}
|
||||
|
||||
private async update(): Promise<void> {
|
||||
|
||||
this.availableAuthenticationProviders = this._authenticationProviders.filter(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id));
|
||||
|
||||
const allAccounts: Map<string, UserDataSyncAccount[]> = new Map<string, UserDataSyncAccount[]>();
|
||||
for (const { id } of this.authenticationProviders) {
|
||||
for (const { id } of this.availableAuthenticationProviders) {
|
||||
const accounts = await this.getAccounts(id);
|
||||
allAccounts.set(id, accounts);
|
||||
}
|
||||
@@ -167,7 +188,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
this._all = allAccounts;
|
||||
const current = this.current;
|
||||
await this.updateToken(current);
|
||||
this.updateAccountStatus(current);
|
||||
this.updateAccountStatus(current ? AccountStatus.Available : AccountStatus.Unavailable);
|
||||
}
|
||||
|
||||
private async getAccounts(authenticationProviderId: string): Promise<UserDataSyncAccount[]> {
|
||||
@@ -195,9 +216,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
let value: { token: string, authenticationProviderId: string } | undefined = undefined;
|
||||
if (current) {
|
||||
try {
|
||||
this.logService.trace('Preferences Sync: Updating the token for the account', current.accountName);
|
||||
this.logService.trace('Settings Sync: Updating the token for the account', current.accountName);
|
||||
const token = current.token;
|
||||
this.logService.trace('Preferences Sync: Token updated for the account', current.accountName);
|
||||
this.logService.trace('Settings Sync: Token updated for the account', current.accountName);
|
||||
value = { token, authenticationProviderId: current.authenticationProviderId };
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
@@ -206,10 +227,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
await this.userDataSyncAccountService.updateAccount(value);
|
||||
}
|
||||
|
||||
private updateAccountStatus(current: UserDataSyncAccount | undefined): void {
|
||||
// set status
|
||||
const accountStatus: AccountStatus = current ? AccountStatus.Available : AccountStatus.Unavailable;
|
||||
|
||||
private updateAccountStatus(accountStatus: AccountStatus): void {
|
||||
if (this._accountStatus !== accountStatus) {
|
||||
const previous = this._accountStatus;
|
||||
this.logService.debug('Sync account status changed', previous, accountStatus);
|
||||
@@ -221,6 +239,13 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
if (this.userDataAutoSyncService.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (this.userDataSyncService.status !== SyncStatus.Idle) {
|
||||
throw new Error('Cannont turn on sync while syncing');
|
||||
}
|
||||
|
||||
const picked = await this.pick();
|
||||
if (!picked) {
|
||||
throw canceled();
|
||||
@@ -231,9 +256,17 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
throw new Error(localize('no account', "No account available"));
|
||||
}
|
||||
|
||||
const preferencesSyncTitle = localize('preferences sync', "Preferences Sync");
|
||||
const title = `${preferencesSyncTitle} [(${localize('details', "details")})](command:${SHOW_SYNC_LOG_COMMAND_ID})`;
|
||||
await this.syncBeforeTurningOn(title);
|
||||
const syncTitle = SYNC_TITLE;
|
||||
const title = `${syncTitle} [(${localize('details', "details")})](command:${SHOW_SYNC_LOG_COMMAND_ID})`;
|
||||
const manualSyncTask = await this.userDataSyncService.createManualSyncTask();
|
||||
const disposable = this.lifecycleService.onBeforeShutdown(e => e.veto(this.onBeforeShutdown(manualSyncTask)));
|
||||
|
||||
try {
|
||||
await this.syncBeforeTurningOn(title, manualSyncTask);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
await this.userDataAutoSyncService.turnOn();
|
||||
this.notificationService.info(localize('sync turned on', "{0} is turned on", title));
|
||||
}
|
||||
@@ -242,15 +275,27 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
return this.userDataAutoSyncService.turnOff(everywhere);
|
||||
}
|
||||
|
||||
private async syncBeforeTurningOn(title: string): Promise<void> {
|
||||
private async onBeforeShutdown(manualSyncTask: IManualSyncTask): Promise<boolean> {
|
||||
const result = await this.dialogService.confirm({
|
||||
type: 'warning',
|
||||
message: localize('sync in progress', "Settings Sync is being turned on. Would you like to cancel it?"),
|
||||
title: localize('settings sync', "Settings Sync"),
|
||||
primaryButton: localize('yes', "Yes"),
|
||||
secondaryButton: localize('no', "No"),
|
||||
});
|
||||
if (result.confirmed) {
|
||||
await manualSyncTask.stop();
|
||||
}
|
||||
return !result.confirmed;
|
||||
}
|
||||
|
||||
private async syncBeforeTurningOn(title: string, manualSyncTask: IManualSyncTask): Promise<void> {
|
||||
|
||||
/* Make sure sync started on clean local state */
|
||||
await this.userDataSyncService.resetLocal();
|
||||
|
||||
const manualSyncTask = await this.userDataSyncService.createManualSyncTask();
|
||||
try {
|
||||
let action: FirstTimeSyncAction = 'manual';
|
||||
let preview: [SyncResource, ISyncResourcePreview][] = [];
|
||||
|
||||
await this.progressService.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
@@ -259,18 +304,24 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}, async progress => {
|
||||
progress.report({ message: localize('turning on', "Turning on...") });
|
||||
|
||||
preview = await manualSyncTask.preview();
|
||||
const preview = await manualSyncTask.preview();
|
||||
const hasRemoteData = manualSyncTask.manifest !== null;
|
||||
const hasLocalData = await this.userDataSyncService.hasLocalData();
|
||||
const hasChanges = preview.some(([, { resourcePreviews }]) => resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None));
|
||||
const isLastSyncFromCurrentMachine = preview.every(([, { isLastSyncFromCurrentMachine }]) => isLastSyncFromCurrentMachine);
|
||||
const hasMergesFromAnotherMachine = preview.some(([syncResource, { isLastSyncFromCurrentMachine, resourcePreviews }]) =>
|
||||
syncResource !== SyncResource.GlobalState && !isLastSyncFromCurrentMachine
|
||||
&& resourcePreviews.some(r => r.localChange !== Change.None || r.remoteChange !== Change.None));
|
||||
|
||||
action = await this.getFirstTimeSyncAction(hasRemoteData, hasLocalData, hasChanges, isLastSyncFromCurrentMachine);
|
||||
action = await this.getFirstTimeSyncAction(hasRemoteData, hasLocalData, hasMergesFromAnotherMachine);
|
||||
const progressDisposable = manualSyncTask.onSynchronizeResources(synchronizingResources =>
|
||||
synchronizingResources.length ? progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(synchronizingResources[0][0])) }) : undefined);
|
||||
try {
|
||||
switch (action) {
|
||||
case 'merge': return await manualSyncTask.apply();
|
||||
case 'merge':
|
||||
await manualSyncTask.merge();
|
||||
if (manualSyncTask.status !== SyncStatus.HasConflicts) {
|
||||
await manualSyncTask.apply();
|
||||
}
|
||||
return;
|
||||
case 'pull': return await manualSyncTask.pull();
|
||||
case 'push': return await manualSyncTask.push();
|
||||
case 'manual': return;
|
||||
@@ -279,8 +330,15 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
progressDisposable.dispose();
|
||||
}
|
||||
});
|
||||
if (manualSyncTask.status === SyncStatus.HasConflicts) {
|
||||
await this.dialogService.show(Severity.Warning, localize('conflicts detected', "Conflicts Detected"), [], {
|
||||
detail: localize('resolve', "Unable to merge due to conflicts. Please merge manually to continue...")
|
||||
});
|
||||
await manualSyncTask.discardConflicts();
|
||||
action = 'manual';
|
||||
}
|
||||
if (action === 'manual') {
|
||||
await this.syncManually(manualSyncTask, preview);
|
||||
await this.syncManually(manualSyncTask);
|
||||
}
|
||||
} catch (error) {
|
||||
await manualSyncTask.stop();
|
||||
@@ -290,12 +348,11 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}
|
||||
}
|
||||
|
||||
private async getFirstTimeSyncAction(hasRemoteData: boolean, hasLocalData: boolean, hasChanges: boolean, isLastSyncFromCurrentMachine: boolean): Promise<FirstTimeSyncAction> {
|
||||
private async getFirstTimeSyncAction(hasRemoteData: boolean, hasLocalData: boolean, hasMergesFromAnotherMachine: boolean): Promise<FirstTimeSyncAction> {
|
||||
|
||||
if (!hasLocalData /* no data on local */
|
||||
|| !hasRemoteData /* no data on remote */
|
||||
|| !hasChanges /* no changes */
|
||||
|| isLastSyncFromCurrentMachine /* has changes but last sync is from current machine */
|
||||
|| !hasMergesFromAnotherMachine /* no merges with another machine */
|
||||
) {
|
||||
return 'merge';
|
||||
}
|
||||
@@ -329,8 +386,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
throw canceled();
|
||||
}
|
||||
|
||||
private async syncManually(task: IManualSyncTask, preview: [SyncResource, ISyncResourcePreview][]): Promise<void> {
|
||||
private async syncManually(task: IManualSyncTask): Promise<void> {
|
||||
const visibleViewContainer = this.viewsService.getVisibleViewContainer(ViewContainerLocation.Sidebar);
|
||||
const preview = await task.preview();
|
||||
this.userDataSyncPreview.setManualSyncPreview(task, preview);
|
||||
|
||||
this.mergesViewEnablementContext.set(true);
|
||||
@@ -371,6 +429,30 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
await this.viewsService.openViewContainer(SYNC_VIEW_CONTAINER_ID);
|
||||
}
|
||||
|
||||
async switchSyncService(type: UserDataSyncStoreType): Promise<void> {
|
||||
if (!this.userDataSyncStoreManagementService.userDataSyncStore
|
||||
|| !this.userDataSyncStoreManagementService.userDataSyncStore.insidersUrl
|
||||
|| !this.userDataSyncStoreManagementService.userDataSyncStore.stableUrl) {
|
||||
return;
|
||||
}
|
||||
await this.userDataSyncStoreManagementService.switch(type);
|
||||
const res = await this.dialogService.confirm({
|
||||
type: 'info',
|
||||
message: isNative ?
|
||||
localize('relaunchMessage', "Switching settings sync service requires a restart to take effect.") :
|
||||
localize('relaunchMessageWeb', "Switching settings sync service requires a reload to take effect."),
|
||||
detail: isNative ?
|
||||
localize('relaunchDetail', "Press the restart button to restart {0} and switch.", this.productService.nameLong) :
|
||||
localize('relaunchDetailWeb', "Press the reload button to reload {0} and switch.", this.productService.nameLong),
|
||||
primaryButton: isNative ?
|
||||
localize('restart', "&&Restart") :
|
||||
localize('restartWeb', "&&Reload"),
|
||||
});
|
||||
if (res.confirmed) {
|
||||
this.hostService.restart();
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForActiveSyncViews(): Promise<void> {
|
||||
const viewContainer = this.viewDescriptorService.getViewContainerById(SYNC_VIEW_CONTAINER_ID);
|
||||
if (viewContainer) {
|
||||
@@ -381,10 +463,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}
|
||||
}
|
||||
|
||||
private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
|
||||
return this.authenticationProviders.some(({ id }) => id === authenticationProviderId);
|
||||
}
|
||||
|
||||
private isCurrentAccount(account: UserDataSyncAccount): boolean {
|
||||
return account.sessionId === this.currentSessionId;
|
||||
}
|
||||
@@ -414,15 +492,15 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}
|
||||
|
||||
private async doPick(): Promise<UserDataSyncAccount | IAuthenticationProvider | undefined> {
|
||||
if (this.authenticationProviders.length === 0) {
|
||||
if (this.availableAuthenticationProviders.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this.update();
|
||||
|
||||
// Single auth provider and no accounts available
|
||||
if (this.authenticationProviders.length === 1 && !this.all.length) {
|
||||
return this.authenticationProviders[0];
|
||||
if (this.availableAuthenticationProviders.length === 1 && !this.all.length) {
|
||||
return this.availableAuthenticationProviders[0];
|
||||
}
|
||||
|
||||
return new Promise<UserDataSyncAccount | IAuthenticationProvider | undefined>(async (c, e) => {
|
||||
@@ -431,7 +509,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
const quickPick = this.quickInputService.createQuickPick<AccountQuickPickItem>();
|
||||
disposables.add(quickPick);
|
||||
|
||||
quickPick.title = localize('pick an account', "Preferences Sync");
|
||||
quickPick.title = SYNC_TITLE;
|
||||
quickPick.ok = false;
|
||||
quickPick.placeholder = localize('choose account placeholder', "Select an account");
|
||||
quickPick.ignoreFocusOut = true;
|
||||
@@ -454,7 +532,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
|
||||
// Signed in Accounts
|
||||
if (this.all.length) {
|
||||
const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1);
|
||||
const authenticationProviders = [...this.availableAuthenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1);
|
||||
quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") });
|
||||
for (const authenticationProvider of authenticationProviders) {
|
||||
const accounts = (this._all.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.current?.sessionId ? -1 : 1);
|
||||
@@ -472,7 +550,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
}
|
||||
|
||||
// Account proviers
|
||||
for (const authenticationProvider of this.authenticationProviders) {
|
||||
for (const authenticationProvider of this.availableAuthenticationProviders) {
|
||||
const signedInForProvider = this.all.some(account => account.authenticationProviderId === authenticationProvider.id);
|
||||
if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) {
|
||||
const providerName = this.authenticationService.getLabel(authenticationProvider.id);
|
||||
@@ -500,7 +578,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: localize('successive auth failures', "Preferences sync was turned off because of successive authorization failures. Please sign in again to continue synchronizing"),
|
||||
message: localize('successive auth failures', "Settings sync was turned off because of successive authorization failures. Please sign in again to continue synchronizing"),
|
||||
actions: {
|
||||
primary: [new Action('sign in', localize('sign in', "Sign in"), undefined, true, () => this.signIn())]
|
||||
}
|
||||
@@ -593,7 +671,7 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview {
|
||||
this.updateResources();
|
||||
}
|
||||
|
||||
async accept(syncResource: SyncResource, resource: URI, content: string | null): Promise<void> {
|
||||
async accept(syncResource: SyncResource, resource: URI, content?: string | null): Promise<void> {
|
||||
if (this.manualSync) {
|
||||
const syncPreview = await this.manualSync.task.accept(resource, content);
|
||||
this.updatePreview(syncPreview);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IAuthenticationProvider, SyncStatus, SyncResource, Change, MergeState } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IAuthenticationProvider, SyncStatus, SyncResource, Change, MergeState, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { localize } from 'vs/nls';
|
||||
@@ -20,7 +20,7 @@ export interface IUserDataSyncPreview {
|
||||
readonly onDidChangeResources: Event<ReadonlyArray<IUserDataSyncResource>>;
|
||||
readonly resources: ReadonlyArray<IUserDataSyncResource>;
|
||||
|
||||
accept(syncResource: SyncResource, resource: URI, content: string | null): Promise<void>;
|
||||
accept(syncResource: SyncResource, resource: URI, content?: string | null): Promise<void>;
|
||||
merge(resource?: URI): Promise<void>;
|
||||
discard(resource?: URI): Promise<void>;
|
||||
pull(): Promise<void>;
|
||||
@@ -44,7 +44,9 @@ export const IUserDataSyncWorkbenchService = createDecorator<IUserDataSyncWorkbe
|
||||
export interface IUserDataSyncWorkbenchService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly enabled: boolean;
|
||||
readonly authenticationProviders: IAuthenticationProvider[];
|
||||
|
||||
readonly all: IUserDataSyncAccount[];
|
||||
readonly current: IUserDataSyncAccount | undefined;
|
||||
|
||||
@@ -56,6 +58,7 @@ export interface IUserDataSyncWorkbenchService {
|
||||
turnOn(): Promise<void>;
|
||||
turnoff(everyWhere: boolean): Promise<void>;
|
||||
signIn(): Promise<void>;
|
||||
switchSyncService(type: UserDataSyncStoreType): Promise<void>;
|
||||
|
||||
resetSyncedData(): Promise<void>;
|
||||
showSyncActivity(): Promise<void>;
|
||||
@@ -77,6 +80,8 @@ export const enum AccountStatus {
|
||||
Available = 'available',
|
||||
}
|
||||
|
||||
export const SYNC_TITLE = localize('sync category', "Settings Sync");
|
||||
|
||||
// Contexts
|
||||
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
|
||||
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService {
|
||||
|
||||
@@ -15,6 +16,8 @@ class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMac
|
||||
|
||||
private readonly channel: IChannel;
|
||||
|
||||
get onDidChange(): Event<void> { return this.channel.listen<void>('onDidChange'); }
|
||||
|
||||
constructor(
|
||||
@ISharedProcessService sharedProcessService: ISharedProcessService
|
||||
) {
|
||||
|
||||
@@ -38,6 +38,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>());
|
||||
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event;
|
||||
|
||||
get onDidResetLocal(): Event<void> { return this.channel.listen<void>('onDidResetLocal'); }
|
||||
get onDidResetRemote(): Event<void> { return this.channel.listen<void>('onDidResetRemote'); }
|
||||
|
||||
constructor(
|
||||
@ISharedProcessService private readonly sharedProcessService: ISharedProcessService
|
||||
) {
|
||||
@@ -65,17 +68,13 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this._register(this.channel.listen<[SyncResource, Error][]>('onSyncErrors')(errors => this._onSyncErrors.fire(errors.map(([source, error]) => ([source, UserDataSyncError.toUserDataSyncError(error)])))));
|
||||
}
|
||||
|
||||
pull(): Promise<void> {
|
||||
return this.channel.call('pull');
|
||||
}
|
||||
|
||||
createSyncTask(): Promise<ISyncTask> {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
async createManualSyncTask(): Promise<IManualSyncTask> {
|
||||
const { id, manifest } = await this.channel.call<{ id: string, manifest: IUserDataManifest | null }>('createManualSyncTask');
|
||||
return new ManualSyncTask(id, manifest, this.sharedProcessService);
|
||||
const { id, manifest, status } = await this.channel.call<{ id: string, manifest: IUserDataManifest | null, status: SyncStatus }>('createManualSyncTask');
|
||||
return new ManualSyncTask(id, manifest, status, this.sharedProcessService);
|
||||
}
|
||||
|
||||
replace(uri: URI): Promise<void> {
|
||||
@@ -120,8 +119,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return handles.map(({ created, uri }) => ({ created, uri: URI.revive(uri) }));
|
||||
}
|
||||
|
||||
async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
const result = await this.channel.call<{ resource: URI, comparableResource?: URI }[]>('getAssociatedResources', [resource, syncResourceHandle]);
|
||||
async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
const result = await this.channel.call<{ resource: URI, comparableResource: URI }[]>('getAssociatedResources', [resource, syncResourceHandle]);
|
||||
return result.map(({ resource, comparableResource }) => ({ resource: URI.revive(resource), comparableResource: URI.revive(comparableResource) }));
|
||||
}
|
||||
|
||||
@@ -164,16 +163,27 @@ class ManualSyncTask implements IManualSyncTask {
|
||||
|
||||
get onSynchronizeResources(): Event<[SyncResource, URI[]][]> { return this.channel.listen<[SyncResource, URI[]][]>('onSynchronizeResources'); }
|
||||
|
||||
private _status: SyncStatus;
|
||||
get status(): SyncStatus { return this._status; }
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly manifest: IUserDataManifest | null,
|
||||
status: SyncStatus,
|
||||
sharedProcessService: ISharedProcessService,
|
||||
) {
|
||||
const manualSyncTaskChannel = sharedProcessService.getChannel(`manualSyncTask-${id}`);
|
||||
this._status = status;
|
||||
const that = this;
|
||||
this.channel = {
|
||||
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
return manualSyncTaskChannel.call(command, arg, cancellationToken)
|
||||
.then(null, error => { throw UserDataSyncError.toUserDataSyncError(error); });
|
||||
async call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
|
||||
try {
|
||||
const result = await manualSyncTaskChannel.call<T>(command, arg, cancellationToken);
|
||||
that._status = await manualSyncTaskChannel.call<SyncStatus>('_getStatus');
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw UserDataSyncError.toUserDataSyncError(error);
|
||||
}
|
||||
},
|
||||
listen<T>(event: string, arg?: any): Event<T> {
|
||||
return manualSyncTaskChannel.listen(event, arg);
|
||||
@@ -186,12 +196,12 @@ class ManualSyncTask implements IManualSyncTask {
|
||||
return this.deserializePreviews(previews);
|
||||
}
|
||||
|
||||
async accept(resource: URI, content: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('accept', [resource, content]);
|
||||
return this.deserializePreviews(previews);
|
||||
}
|
||||
|
||||
async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('merge', [resource]);
|
||||
return this.deserializePreviews(previews);
|
||||
}
|
||||
@@ -201,6 +211,11 @@ class ManualSyncTask implements IManualSyncTask {
|
||||
return this.deserializePreviews(previews);
|
||||
}
|
||||
|
||||
async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('discardConflicts');
|
||||
return this.deserializePreviews(previews);
|
||||
}
|
||||
|
||||
async apply(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const previews = await this.channel.call<[SyncResource, ISyncResourcePreview][]>('apply');
|
||||
return this.deserializePreviews(previews);
|
||||
|
||||
Reference in New Issue
Block a user