Update to VS Code 1.52.1

This commit is contained in:
Asher
2021-02-09 16:08:37 +00:00
1351 changed files with 56560 additions and 38990 deletions

View File

@@ -14,7 +14,7 @@ import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
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 { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IProductService } from 'vs/platform/product/common/productService';
import { isString } from 'vs/base/common/types';
@@ -375,11 +375,11 @@ export class AuthenticationService extends Disposable implements IAuthentication
const allowList = readAllowedExtensions(storageService, providerId, session.account.label);
if (!allowList.find(allowed => allowed.id === extensionId)) {
allowList.push({ id: extensionId, name: extensionName });
storageService.store(`${providerId}-${session.account.label}`, JSON.stringify(allowList), StorageScope.GLOBAL);
storageService.store(`${providerId}-${session.account.label}`, JSON.stringify(allowList), StorageScope.GLOBAL, StorageTarget.USER);
}
// And also set it as the preferred account for the extension
storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL);
storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
});

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { ILogService } from 'vs/platform/log/common/log';
import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
import { hash } from 'vs/base/common/hash';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { joinPath } from 'vs/base/common/resources';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
export class BrowserBackupFileService extends BackupFileService {
declare readonly _serviceBrand: undefined;
constructor(
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IFileService fileService: IFileService,
@ILogService logService: ILogService
) {
super(joinPath(environmentService.userRoamingDataHome, 'Backups', contextService.getWorkspace().id), fileService, logService);
}
protected hashPath(resource: URI): string {
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
return hash(str).toString(16);
}
}
registerSingleton(IBackupFileService, BrowserBackupFileService);

View File

@@ -6,7 +6,6 @@
import { join } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { hash } from 'vs/base/common/hash';
import { coalesce } from 'vs/base/common/arrays';
import { equals, deepClone } from 'vs/base/common/objects';
import { ResourceQueue } from 'vs/base/common/async';
@@ -15,8 +14,6 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
import { ITextSnapshot } from 'vs/editor/common/model';
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { VSBuffer } from 'vs/base/common/buffer';
import { TextSnapshotReadable, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -108,42 +105,36 @@ export class BackupFilesModel implements IBackupFilesModel {
}
}
export class BackupFileService implements IBackupFileService {
export abstract class BackupFileService implements IBackupFileService {
declare readonly _serviceBrand: undefined;
private impl: BackupFileServiceImpl | InMemoryBackupFileService;
constructor(
@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService,
backupWorkspaceHome: URI | undefined,
@IFileService protected fileService: IFileService,
@ILogService private readonly logService: ILogService
) {
this.impl = this.initialize();
this.impl = this.initialize(backupWorkspaceHome);
}
protected hashPath(resource: URI): string {
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
protected abstract hashPath(resource: URI): string;
return hash(str).toString(16);
}
private initialize(): BackupFileServiceImpl | InMemoryBackupFileService {
const backupWorkspaceResource = this.environmentService.backupWorkspaceHome;
if (backupWorkspaceResource) {
return new BackupFileServiceImpl(backupWorkspaceResource, this.hashPath, this.fileService, this.logService);
private initialize(backupWorkspaceHome: URI | undefined): BackupFileServiceImpl | InMemoryBackupFileService {
if (backupWorkspaceHome) {
return new BackupFileServiceImpl(backupWorkspaceHome, this.hashPath, this.fileService, this.logService);
}
return new InMemoryBackupFileService(this.hashPath);
}
reinitialize(): void {
reinitialize(backupWorkspaceHome: URI | undefined): void {
// Re-init implementation (unless we are running in-memory)
if (this.impl instanceof BackupFileServiceImpl) {
const backupWorkspaceResource = this.environmentService.backupWorkspaceHome;
if (backupWorkspaceResource) {
this.impl.initialize(backupWorkspaceResource);
if (backupWorkspaceHome) {
this.impl.initialize(backupWorkspaceHome);
} else {
this.impl = new InMemoryBackupFileService(this.hashPath);
}

View File

@@ -3,14 +3,25 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { BackupFileService as CommonBackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import * as crypto from 'crypto';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
export class BackupFileService extends CommonBackupFileService {
export class NativeBackupFileService extends BackupFileService {
constructor(
@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
@IFileService fileService: IFileService,
@ILogService logService: ILogService
) {
super(environmentService.configuration.backupPath ? URI.file(environmentService.configuration.backupPath).with({ scheme: environmentService.userRoamingDataHome.scheme }) : undefined, fileService, logService);
}
protected hashPath(resource: URI): string {
return hashPath(resource);
@@ -26,4 +37,4 @@ export function hashPath(resource: URI): string {
return crypto.createHash('md5').update(str).digest('hex');
}
registerSingleton(IBackupFileService, BackupFileService);
registerSingleton(IBackupFileService, NativeBackupFileService);

View File

@@ -23,7 +23,7 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
import { hashPath, BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
import { hashPath, NativeBackupFileService } from 'vs/workbench/services/backup/electron-browser/backupFileService';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { VSBuffer } from 'vs/base/common/buffer';
import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices';
@@ -53,7 +53,7 @@ class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService
}
}
export class NodeTestBackupFileService extends BackupFileService {
export class NodeTestBackupFileService extends NativeBackupFileService {
readonly fileService: IFileService;
@@ -67,7 +67,7 @@ export class NodeTestBackupFileService extends BackupFileService {
const fileService = new FileService(logService);
const diskFileSystemProvider = new DiskFileSystemProvider(logService);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, URI.file(workspaceBackupPath), diskFileSystemProvider, environmentService, logService));
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService));
super(environmentService, fileService, logService);

View File

@@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri';
import * as resources from 'vs/base/common/resources';
import { Event, Emitter } from 'vs/base/common/event';
import * as errors from 'vs/base/common/errors';
import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { ConfigurationModel, ConfigurationModelParser, UserSettings } from 'vs/platform/configuration/common/configurationModels';
@@ -92,6 +92,7 @@ class FileServiceBasedConfiguration extends Disposable {
) {
super();
this.allResources = [...this.settingsResources, ...this.standAloneConfigurationResources.map(([, resource]) => resource)];
this._register(combinedDisposable(...this.allResources.map(resource => this.fileService.watch(resources.dirname(resource)))));
this._folderSettingsModelParser = new ConfigurationModelParser(name, this.scopes);
this._standAloneConfigurations = [];
this._cache = new ConfigurationModel();

View File

@@ -32,6 +32,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
class Workspace extends BaseWorkspace {
initialized: boolean = false;
@@ -55,6 +56,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
private readonly logService: ILogService;
private readonly fileService: IFileService;
private readonly uriIdentityService: IUriIdentityService;
protected readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
public readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
@@ -79,6 +81,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
environmentService: IWorkbenchEnvironmentService,
fileService: IFileService,
remoteAgentService: IRemoteAgentService,
uriIdentityService: IUriIdentityService,
logService: ILogService,
) {
super();
@@ -94,6 +97,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
this.defaultConfiguration = new DefaultConfigurationModel();
this.configurationCache = configurationCache;
this.fileService = fileService;
this.uriIdentityService = uriIdentityService;
this.logService = logService;
this._configuration = new Configuration(this.defaultConfiguration, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace);
this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>();
@@ -285,13 +289,38 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
});
}
reloadConfiguration(folder?: IWorkspaceFolder, key?: string): Promise<void> {
if (folder) {
return this.reloadWorkspaceFolderConfiguration(folder, key);
async reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise<void> {
if (target === undefined) {
const { local, remote } = await this.reloadUserConfiguration();
await this.reloadWorkspaceConfiguration();
await this.loadConfiguration(local, remote);
return;
}
if (IWorkspaceFolder.isIWorkspaceFolder(target)) {
await this.reloadWorkspaceFolderConfiguration(target);
return;
}
switch (target) {
case ConfigurationTarget.USER:
const { local, remote } = await this.reloadUserConfiguration();
await this.loadConfiguration(local, remote);
return;
case ConfigurationTarget.USER_LOCAL:
await this.reloadLocalUserConfiguration();
return;
case ConfigurationTarget.USER_REMOTE:
await this.reloadRemoteUserConfiguration();
return;
case ConfigurationTarget.WORKSPACE:
case ConfigurationTarget.WORKSPACE_FOLDER:
await this.reloadWorkspaceConfiguration();
return;
}
return this.reloadUserConfiguration()
.then(({ local, remote }) => this.reloadWorkspaceConfiguration()
.then(() => this.loadConfiguration(local, remote)));
}
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> {
@@ -346,20 +375,20 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
const workspaceConfigPath = workspaceIdentifier.configPath;
const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), workspaceConfigPath);
const workspaceId = workspaceIdentifier.id;
const workspace = new Workspace(workspaceId, workspaceFolders, workspaceConfigPath);
const workspace = new Workspace(workspaceId, workspaceFolders, workspaceConfigPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri));
workspace.initialized = this.workspaceConfiguration.initialized;
return workspace;
});
}
private createSingleFolderWorkspace(singleFolder: ISingleFolderWorkspaceInitializationPayload): Promise<Workspace> {
const workspace = new Workspace(singleFolder.id, [toWorkspaceFolder(singleFolder.folder)]);
const workspace = new Workspace(singleFolder.id, [toWorkspaceFolder(singleFolder.folder)], null, uri => this.uriIdentityService.extUri.ignorePathCasing(uri));
workspace.initialized = true;
return Promise.resolve(workspace);
}
private createEmptyWorkspace(emptyWorkspace: IEmptyWorkspaceInitializationPayload): Promise<Workspace> {
const workspace = new Workspace(emptyWorkspace.id);
const workspace = new Workspace(emptyWorkspace.id, [], null, uri => this.uriIdentityService.extUri.ignorePathCasing(uri));
workspace.initialized = true;
return Promise.resolve(workspace);
}
@@ -465,10 +494,10 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
return new ConfigurationModel();
}
private reloadWorkspaceConfiguration(key?: string): Promise<void> {
private reloadWorkspaceConfiguration(): Promise<void> {
const workbenchState = this.getWorkbenchState();
if (workbenchState === WorkbenchState.FOLDER) {
return this.onWorkspaceFolderConfigurationChanged(this.workspace.folders[0], key);
return this.onWorkspaceFolderConfigurationChanged(this.workspace.folders[0]);
}
if (workbenchState === WorkbenchState.WORKSPACE) {
return this.workspaceConfiguration.reload().then(() => this.onWorkspaceConfigurationChanged());
@@ -476,8 +505,8 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
return Promise.resolve(undefined);
}
private reloadWorkspaceFolderConfiguration(folder: IWorkspaceFolder, key?: string): Promise<void> {
return this.onWorkspaceFolderConfigurationChanged(folder, key);
private reloadWorkspaceFolderConfiguration(folder: IWorkspaceFolder): Promise<void> {
return this.onWorkspaceFolderConfigurationChanged(folder);
}
private loadConfiguration(userConfigurationModel: ConfigurationModel, remoteUserConfigurationModel: ConfigurationModel): Promise<void> {
@@ -592,7 +621,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
}
}
private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder, key?: string): Promise<void> {
private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder): Promise<void> {
return this.loadFolderConfigurations([folder])
.then(([folderConfiguration]) => {
const previous = { data: this._configuration.toData(), workspace: this.workspace };
@@ -700,7 +729,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
case EditableConfigurationTarget.WORKSPACE_FOLDER:
const workspaceFolder = overrides && overrides.resource ? this.workspace.getFolder(overrides.resource) : null;
if (workspaceFolder) {
return this.reloadWorkspaceFolderConfiguration(workspaceFolder, key);
return this.reloadWorkspaceFolderConfiguration(workspaceFolder);
}
}
return Promise.resolve();

View File

@@ -305,7 +305,7 @@ export class ConfigurationEditingService {
}
private openFile(resource: URI): void {
this.editorService.openEditor({ resource });
this.editorService.openEditor({ resource, options: { pinned: true } });
}
private reject<T = never>(code: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): Promise<T> {

View File

@@ -8,8 +8,9 @@ import { StandaloneConfigurationModelParser, Configuration } from 'vs/workbench/
import { ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { ResourceMap } from 'vs/base/common/map';
import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace';
suite('FolderSettingsModelParser', () => {

View File

@@ -41,6 +41,8 @@ import { ConfigurationCache } from 'vs/workbench/services/configuration/electron
import { KeybindingsEditingService, IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import { DisposableStore } from 'vs/base/common/lifecycle';
class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService {
@@ -61,6 +63,8 @@ suite('ConfigurationEditingService', () => {
let globalTasksFile: string;
let workspaceSettingsDir;
const disposables = new DisposableStore();
suiteSetup(() => {
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
@@ -83,9 +87,9 @@ suite('ConfigurationEditingService', () => {
});
});
setup(() => {
return setUpWorkspace()
.then(() => setUpServices());
setup(async () => {
await setUpWorkspace();
await setUpServices();
});
async function setUpWorkspace(): Promise<void> {
@@ -99,50 +103,36 @@ suite('ConfigurationEditingService', () => {
return await mkdirp(workspaceSettingsDir, 493);
}
function setUpServices(noWorkspace: boolean = false): Promise<void> {
// Clear services if they are already created
clearServices();
async function setUpServices(noWorkspace: boolean = false): Promise<void> {
instantiationService = <TestInstantiationService>workbenchInstantiationService();
const environmentService = new TestWorkbenchEnvironmentService(URI.file(workspaceDir));
instantiationService.stub(IEnvironmentService, environmentService);
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
instantiationService.stub(IFileService, fileService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService());
const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, workspaceService);
return workspaceService.initialize(noWorkspace ? { id: '' } : { folder: URI.file(workspaceDir), id: createHash('md5').update(URI.file(workspaceDir).toString()).digest('hex') }).then(() => {
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IKeybindingEditingService, instantiationService.createInstance(KeybindingsEditingService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(ICommandService, CommandService);
testObject = instantiationService.createInstance(ConfigurationEditingService);
});
await workspaceService.initialize(noWorkspace ? { id: '' } : { folder: URI.file(workspaceDir), id: createHash('md5').update(URI.file(workspaceDir).toString()).digest('hex') });
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IKeybindingEditingService, instantiationService.createInstance(KeybindingsEditingService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(ICommandService, CommandService);
testObject = instantiationService.createInstance(ConfigurationEditingService);
}
teardown(() => {
clearServices();
disposables.clear();
if (workspaceDir) {
return rimraf(workspaceDir, RimRafMode.MOVE);
}
return undefined;
});
function clearServices(): void {
if (instantiationService) {
const configuraitonService = <WorkspaceService>instantiationService.get(IConfigurationService);
if (configuraitonService) {
configuraitonService.dispose();
}
instantiationService = null!;
}
}
test('errors cases - invalid key', () => {
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' })
.then(() => assert.fail('Should fail with ERROR_UNKNOWN_KEY'),

View File

@@ -54,6 +54,7 @@ import product from 'vs/platform/product/common/product';
import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { Event } from 'vs/base/common/event';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService {
@@ -105,6 +106,7 @@ function setUpWorkspace(folders: string[]): Promise<{ parentDir: string, configP
suite('WorkspaceContextService - Folder', () => {
let workspaceName = `testWorkspace${uuid.generateUuid()}`, parentResource: string, workspaceResource: string, workspaceContextService: IWorkspaceContextService;
const disposables = new DisposableStore();
setup(() => {
return setUpFolderWorkspace(workspaceName)
@@ -112,19 +114,17 @@ suite('WorkspaceContextService - Folder', () => {
parentResource = parentDir;
workspaceResource = folderDir;
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, new DiskFileSystemProvider(new NullLogService()), environmentService, new NullLogService()));
workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, { _serviceBrand: undefined, ...product }, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService()), new NullLogService());
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()), Schemas.userData, new NullLogService())));
workspaceContextService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, { _serviceBrand: undefined, ...product }, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService()));
return (<WorkspaceService>workspaceContextService).initialize(convertToWorkspacePayload(URI.file(folderDir)));
});
});
teardown(() => {
if (workspaceContextService) {
(<WorkspaceService>workspaceContextService).dispose();
}
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -167,6 +167,7 @@ suite('WorkspaceContextService - Folder', () => {
suite('WorkspaceContextService - Workspace', () => {
let parentResource: string, testObject: WorkspaceService, instantiationService: TestInstantiationService;
const disposables = new DisposableStore();
setup(() => {
return setUpWorkspace(['a', 'b'])
@@ -178,11 +179,11 @@ suite('WorkspaceContextService - Workspace', () => {
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService());
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IConfigurationService, workspaceService);
@@ -196,9 +197,7 @@ suite('WorkspaceContextService - Workspace', () => {
});
teardown(() => {
if (testObject) {
(<WorkspaceService>testObject).dispose();
}
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -227,6 +226,7 @@ suite('WorkspaceContextService - Workspace', () => {
suite('WorkspaceContextService - Workspace Editing', () => {
let parentResource: string, testObject: WorkspaceService, instantiationService: TestInstantiationService;
const disposables = new DisposableStore();
setup(() => {
return setUpWorkspace(['a', 'b'])
@@ -238,11 +238,11 @@ suite('WorkspaceContextService - Workspace Editing', () => {
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService());
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IConfigurationService, workspaceService);
@@ -260,9 +260,7 @@ suite('WorkspaceContextService - Workspace Editing', () => {
});
teardown(() => {
if (testObject) {
(<WorkspaceService>testObject).dispose();
}
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -467,6 +465,7 @@ suite('WorkspaceService - Initialization', () => {
let parentResource: string, workspaceConfigPath: URI, testObject: WorkspaceService, globalSettingsFile: string;
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
const disposables = new DisposableStore();
suiteSetup(() => {
configurationRegistry.registerConfiguration({
@@ -499,11 +498,11 @@ suite('WorkspaceService - Initialization', () => {
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService());
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IEnvironmentService, environmentService);
@@ -519,9 +518,7 @@ suite('WorkspaceService - Initialization', () => {
});
teardown(() => {
if (testObject) {
(<WorkspaceService>testObject).dispose();
}
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -727,7 +724,7 @@ suite('WorkspaceConfigurationService - Folder', () => {
let workspaceName = `testWorkspace${uuid.generateUuid()}`, parentResource: string, workspaceDir: string, testObject: IConfigurationService, globalSettingsFile: string, globalTasksFile: string, workspaceService: WorkspaceService;
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
let fileService: IFileService;
let disposableStore: DisposableStore = new DisposableStore();
const disposables: DisposableStore = new DisposableStore();
suiteSetup(() => {
configurationRegistry.registerConfiguration({
@@ -776,17 +773,17 @@ suite('WorkspaceConfigurationService - Folder', () => {
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
workspaceService = disposableStore.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService()));
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IEnvironmentService, environmentService);
// Watch workspace configuration directory
disposableStore.add(fileService.watch(joinPath(URI.file(workspaceDir), '.vscode')));
disposables.add(fileService.watch(joinPath(URI.file(workspaceDir), '.vscode')));
return workspaceService.initialize(convertToWorkspacePayload(URI.file(folderDir))).then(() => {
instantiationService.stub(IFileService, fileService);
@@ -800,7 +797,7 @@ suite('WorkspaceConfigurationService - Folder', () => {
});
teardown(() => {
disposableStore.clear();
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -1232,6 +1229,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => {
let parentResource: string, workspaceContextService: IWorkspaceContextService, jsonEditingServce: IJSONEditingService, testObject: IConfigurationService, globalSettingsFile: string;
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
const disposables = new DisposableStore();
suiteSetup(() => {
configurationRegistry.registerConfiguration({
@@ -1282,11 +1280,11 @@ suite('WorkspaceConfigurationService-Multiroot', () => {
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
instantiationService.stub(IRemoteAgentService, remoteAgentService);
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
const diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService());
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IConfigurationService, workspaceService);
@@ -1308,9 +1306,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => {
});
teardown(() => {
if (testObject) {
(<WorkspaceService>testObject).dispose();
}
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -1841,7 +1837,8 @@ suite('WorkspaceConfigurationService - Remote Folder', () => {
let workspaceName = `testWorkspace${uuid.generateUuid()}`, parentResource: string, workspaceDir: string, testObject: WorkspaceService, globalSettingsFile: string, remoteSettingsFile: string, remoteSettingsResource: URI, instantiationService: TestInstantiationService, resolveRemoteEnvironment: () => void;
const remoteAuthority = 'configuraiton-tests';
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
const disposables = new DisposableStore();
let diskFileSystemProvider: DiskFileSystemProvider;
suiteSetup(() => {
configurationRegistry.registerConfiguration({
@@ -1886,11 +1883,12 @@ suite('WorkspaceConfigurationService - Remote Folder', () => {
const environmentService = new TestWorkbenchEnvironmentService(URI.file(parentDir));
const remoteEnvironmentPromise = new Promise<Partial<IRemoteAgentEnvironment>>(c => resolveRemoteEnvironment = () => c({ settingsPath: remoteSettingsResource }));
const remoteAgentService = instantiationService.stub(IRemoteAgentService, <Partial<IRemoteAgentService>>{ getEnvironment: () => remoteEnvironmentPromise });
const fileService = new FileService(new NullLogService());
const fileService = disposables.add(new FileService(new NullLogService()));
diskFileSystemProvider = disposables.add(new DiskFileSystemProvider(new NullLogService()));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService())));
const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve(), needsCaching: () => false };
testObject = new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new NullLogService());
testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
instantiationService.stub(IWorkspaceContextService, testObject);
instantiationService.stub(IConfigurationService, testObject);
instantiationService.stub(IEnvironmentService, environmentService);
@@ -1919,9 +1917,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => {
}
teardown(() => {
if (testObject) {
(<WorkspaceService>testObject).dispose();
}
disposables.clear();
if (parentResource) {
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
}
@@ -2096,7 +2092,7 @@ suite('ConfigurationService - Configuration Defaults', () => {
const remoteAgentService = (<TestInstantiationService>workbenchInstantiationService()).createInstance(RemoteAgentService);
const environmentService = new BrowserWorkbenchEnvironmentService({ logsPath: URI.file(''), workspaceId: '', configurationDefaults }, TestProductService);
const fileService = new FileService(new NullLogService());
return disposableStore.add(new WorkspaceService({ configurationCache: new BrowserConfigurationCache() }, environmentService, fileService, remoteAgentService, new NullLogService()));
return disposableStore.add(new WorkspaceService({ configurationCache: new BrowserConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService()));
}
});

View File

@@ -44,7 +44,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR
getWorkspaceFolderCount: (): number => {
return workspaceContextService.getWorkspace().folders.length;
},
getConfigurationValue: (folderUri: uri, suffix: string): string | undefined => {
getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => {
return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : {});
},
getExecPath: (): string | undefined => {
@@ -60,6 +60,20 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR
}
return this.labelService.getUriLabel(fileResource, { noPrefix: true });
},
getWorkspaceFolderPathForFile: (): string | undefined => {
const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, {
supportSideBySide: SideBySideEditor.PRIMARY,
filterByScheme: [Schemas.file, Schemas.userData, Schemas.vscodeRemote]
});
if (!fileResource) {
return undefined;
}
const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource);
if (!wsFolder) {
return undefined;
}
return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true });
},
getSelectedText: (): string | undefined => {
const activeTextEditorControl = editorService.activeTextEditorControl;
if (isCodeEditor(activeTextEditorControl)) {

View File

@@ -19,15 +19,17 @@ import { ILabelService } from 'vs/platform/label/common/label';
export interface IVariableResolveContext {
getFolderUri(folderName: string): uri | undefined;
getWorkspaceFolderCount(): number;
getConfigurationValue(folderUri: uri, section: string): string | undefined;
getConfigurationValue(folderUri: uri | undefined, section: string): string | undefined;
getExecPath(): string | undefined;
getFilePath(): string | undefined;
getWorkspaceFolderPathForFile?(): string | undefined;
getSelectedText(): string | undefined;
getLineNumber(): string | undefined;
}
export class AbstractVariableResolverService implements IConfigurationResolverService {
static readonly VARIABLE_LHS = '${';
static readonly VARIABLE_REGEXP = /\$\{(.*?)\}/g;
declare readonly _serviceBrand: undefined;
@@ -38,7 +40,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
protected _contributedVariables: Map<string, () => Promise<string | undefined>> = new Map();
constructor(_context: IVariableResolveContext, _labelService?: ILabelService, _envVariables?: IProcessEnvironment, private _ignoreEditorVariables = false) {
constructor(_context: IVariableResolveContext, _labelService?: ILabelService, _envVariables?: IProcessEnvironment) {
this._context = _context;
this._labelService = _labelService;
if (_envVariables) {
@@ -130,6 +132,10 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
// loop through all variables occurrences in 'value'
const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => {
// disallow attempted nesting, see #77289
if (variable.includes(AbstractVariableResolverService.VARIABLE_LHS)) {
return match;
}
let resolvedValue = this.evaluateSingleVariable(match, variable, folderUri, commandValueMapping);
@@ -164,18 +170,31 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
if (filePath) {
return filePath;
}
throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match));
throw new Error(localize('canNotResolveFile', "Variable {0} can not be resolved. Please open an editor.", match));
};
// common error handling for all variables that require an open editor
const getFolderPathForFile = (): string => {
const filePath = getFilePath(); // throws error if no editor open
if (this._context.getWorkspaceFolderPathForFile) {
const folderPath = this._context.getWorkspaceFolderPathForFile();
if (folderPath) {
return folderPath;
}
}
throw new Error(localize('canNotResolveFolderForFile', "Variable {0}: can not find workspace folder of '{1}'.", match, paths.basename(filePath)));
};
// common error handling for all variables that require an open folder and accept a folder name argument
const getFolderUri = (withArg = true): uri => {
const getFolderUri = (): uri => {
if (withArg && argument) {
if (argument) {
const folder = this._context.getFolderUri(argument);
if (folder) {
return folder;
}
throw new Error(localize('canNotFindFolder', "'{0}' can not be resolved. No such folder '{1}'.", match, argument));
throw new Error(localize('canNotFindFolder', "Variable {0} can not be resolved. No such folder '{1}'.", match, argument));
}
if (folderUri) {
@@ -183,9 +202,9 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
}
if (this._context.getWorkspaceFolderCount() > 1) {
throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match));
throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "Variable {0} can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match));
}
throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match));
throw new Error(localize('canNotResolveWorkspaceFolder', "Variable {0} can not be resolved. Please open a folder.", match));
};
@@ -202,20 +221,20 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
// For `env` we should do the same as a normal shell does - evaluates undefined envs to an empty string #46436
return '';
}
throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match));
throw new Error(localize('missingEnvVarName', "Variable {0} can not be resolved because no environment variable name is given.", match));
case 'config':
if (argument) {
const config = this._context.getConfigurationValue(getFolderUri(false), argument);
const config = this._context.getConfigurationValue(folderUri, argument);
if (types.isUndefinedOrNull(config)) {
throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument));
throw new Error(localize('configNotFound', "Variable {0} can not be resolved because setting '{1}' not found.", match, argument));
}
if (types.isObject(config)) {
throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument));
throw new Error(localize('configNoString', "Variable {0} can not be resolved because '{1}' is a structured value.", match, argument));
}
return config;
}
throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match));
throw new Error(localize('missingConfigName', "Variable {0} can not be resolved because no settings name is given.", match));
case 'command':
return this.resolveFromMap(match, argument, commandValueMapping, 'command');
@@ -238,44 +257,32 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
return paths.basename(this.fsPath(getFolderUri()));
case 'lineNumber':
if (this._ignoreEditorVariables) {
return match;
}
const lineNumber = this._context.getLineNumber();
if (lineNumber) {
return lineNumber;
}
throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match));
throw new Error(localize('canNotResolveLineNumber', "Variable {0} can not be resolved. Make sure to have a line selected in the active editor.", match));
case 'selectedText':
if (this._ignoreEditorVariables) {
return match;
}
const selectedText = this._context.getSelectedText();
if (selectedText) {
return selectedText;
}
throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match));
throw new Error(localize('canNotResolveSelectedText', "Variable {0} can not be resolved. Make sure to have some text selected in the active editor.", match));
case 'file':
if (this._ignoreEditorVariables) {
return match;
}
return getFilePath();
case 'fileWorkspaceFolder':
return getFolderPathForFile();
case 'relativeFile':
if (this._ignoreEditorVariables) {
return match;
}
if (folderUri || argument) {
return paths.relative(this.fsPath(getFolderUri()), getFilePath());
}
return getFilePath();
case 'relativeFileDirname':
if (this._ignoreEditorVariables) {
return match;
}
const dirname = paths.dirname(getFilePath());
if (folderUri || argument) {
const relative = paths.relative(this.fsPath(getFolderUri()), dirname);
@@ -284,30 +291,21 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
return dirname;
case 'fileDirname':
if (this._ignoreEditorVariables) {
return match;
}
return paths.dirname(getFilePath());
case 'fileExtname':
if (this._ignoreEditorVariables) {
return match;
}
return paths.extname(getFilePath());
case 'fileBasename':
if (this._ignoreEditorVariables) {
return match;
}
return paths.basename(getFilePath());
case 'fileBasenameNoExtension':
if (this._ignoreEditorVariables) {
return match;
}
const basename = paths.basename(getFilePath());
return (basename.slice(0, basename.length - paths.extname(basename).length));
case 'fileDirnameBasename':
return paths.basename(paths.dirname(getFilePath()));
case 'execPath':
const ep = this._context.getExecPath();
if (ep) {
@@ -315,6 +313,9 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
}
return match;
case 'pathSeparator':
return paths.sep;
default:
try {
return this.resolveFromMap(match, variable, commandValueMapping, undefined);
@@ -332,7 +333,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
if (typeof v === 'string') {
return v;
}
throw new Error(localize('noValueForCommand', "'{0}' can not be resolved because the command has no value.", match));
throw new Error(localize('noValueForCommand', "Variable {0} can not be resolved because the command has no value.", match));
}
return match;
}

View File

@@ -175,6 +175,10 @@ suite('Configuration Resolver Service', () => {
}
});
test('disallows nested keys (#77289)', () => {
assert.strictEqual(configurationResolverService!.resolve(workspace, '${env:key1} ${env:key1${env:key2}}'), 'Value for key1 ${env:key1${env:key2}}');
});
// test('substitute keys and values in object', () => {
// const myObject = {
// '${workspaceRootFolderName}': '${lineNumber}',
@@ -211,6 +215,17 @@ suite('Configuration Resolver Service', () => {
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');
});
test('substitute configuration variable with undefined workspace folder', () => {
let configurationService: IConfigurationService = new TestConfigurationService({
editor: {
fontFamily: 'foo'
}
});
let service = new TestConfigurationResolverService({ getExecPath: () => undefined }, environmentService.userEnv, new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService);
assert.strictEqual(service.resolve(undefined, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');
});
test('substitute many configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new TestConfigurationService({

View File

@@ -44,7 +44,7 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
super();
// Custom context menu: Linux/Windows if custom title is enabled
if (!isMacintosh && getTitleBarStyle(configurationService, environmentService) === 'custom') {
if (!isMacintosh && getTitleBarStyle(configurationService) === 'custom') {
this.impl = new HTMLContextMenuService(telemetryService, notificationService, contextViewService, keybindingService, themeService);
}

View File

@@ -18,6 +18,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { hash } from 'vs/base/common/hash';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
class DecorationRule {
@@ -106,9 +107,7 @@ class DecorationStyles {
private readonly _decorationRules = new Map<string, DecorationRule>();
private readonly _dispoables = new DisposableStore();
constructor(
private _themeService: IThemeService,
) {
constructor(private readonly _themeService: IThemeService) {
this._themeService.onDidColorThemeChange(this._onThemeChange, this, this._dispoables);
}
@@ -149,7 +148,7 @@ class DecorationStyles {
badgeClassName,
tooltip,
dispose: () => {
if (rule && rule.release()) {
if (rule?.release()) {
this._decorationRules.delete(key);
rule.removeCSSRules(this._styleElement);
rule = undefined;
@@ -168,13 +167,13 @@ class DecorationStyles {
class FileDecorationChangeEvent implements IResourceDecorationChangeEvent {
private readonly _data = TernarySearchTree.forUris<boolean>();
private readonly _data = TernarySearchTree.forUris<true>(_uri => true); // events ignore all path casings
affectsResource(uri: URI): boolean {
return this._data.get(uri) || this._data.findSuperstr(uri) !== undefined;
return this._data.get(uri) ?? this._data.findSuperstr(uri) !== undefined;
}
static debouncer(last: FileDecorationChangeEvent | undefined, current: URI | URI[]) {
static debouncer(last: FileDecorationChangeEvent | undefined, current: URI | URI[]): FileDecorationChangeEvent {
if (!last) {
last = new FileDecorationChangeEvent();
}
@@ -201,14 +200,18 @@ class DecorationDataRequest {
class DecorationProviderWrapper {
readonly data = TernarySearchTree.forUris<DecorationDataRequest | IDecorationData | null>();
readonly data: TernarySearchTree<URI, DecorationDataRequest | IDecorationData | null>;
private readonly _dispoable: IDisposable;
constructor(
readonly provider: IDecorationsProvider,
uriIdentityService: IUriIdentityService,
private readonly _uriEmitter: Emitter<URI | URI[]>,
private readonly _flushEmitter: Emitter<IResourceDecorationChangeEvent>
) {
this.data = TernarySearchTree.forUris(uri => uriIdentityService.extUri.ignorePathCasing(uri));
this._dispoable = this.provider.onDidChange(uris => {
if (!uris) {
// flush event -> drop all data, can affect everything
@@ -233,7 +236,7 @@ class DecorationProviderWrapper {
}
knowsAbout(uri: URI): boolean {
return Boolean(this.data.get(uri)) || Boolean(this.data.findSuperstr(uri));
return this.data.has(uri) || Boolean(this.data.findSuperstr(uri));
}
getOrRetrieve(uri: URI, includeChildren: boolean, callback: (data: IDecorationData, isChild: boolean) => void): void {
@@ -254,9 +257,9 @@ class DecorationProviderWrapper {
// (resolved) children
const iter = this.data.findSuperstr(uri);
if (iter) {
for (let item = iter.next(); !item.done; item = iter.next()) {
if (item.value && !(item.value instanceof DecorationDataRequest)) {
callback(item.value, true);
for (const [, value] of iter) {
if (value && !(value instanceof DecorationDataRequest)) {
callback(value, true);
}
}
}
@@ -326,6 +329,7 @@ export class DecorationsService implements IDecorationsService {
constructor(
@IThemeService themeService: IThemeService,
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
) {
this._decorationStyles = new DecorationStyles(themeService);
}
@@ -340,6 +344,7 @@ export class DecorationsService implements IDecorationsService {
const wrapper = new DecorationProviderWrapper(
provider,
this._uriIdentityService,
this._onDidChangeDecorationsDelayed,
this._onDidChangeDecorations
);

View File

@@ -8,8 +8,11 @@ import { DecorationsService } from 'vs/workbench/services/decorations/browser/de
import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import * as resources from 'vs/base/common/resources';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { mock } from 'vs/base/test/common/mock';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
suite('DecorationsService', function () {
@@ -19,7 +22,12 @@ suite('DecorationsService', function () {
if (service) {
service.dispose();
}
service = new DecorationsService(new TestThemeService());
service = new DecorationsService(
new TestThemeService(),
new class extends mock<IUriIdentityService>() {
extUri = resources.extUri;
}
);
});
test('Async provider, async/evented result', function () {

View File

@@ -24,6 +24,7 @@ import { trim } from 'vs/base/common/strings';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ILabelService } from 'vs/platform/label/common/label';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { Schemas } from 'vs/base/common/network';
export abstract class AbstractFileDialogService implements IFileDialogService {
@@ -45,7 +46,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
@IPathService private readonly pathService: IPathService
) { }
defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
async defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> {
// Check for last active file first...
let candidate = this.historyService.getLastActiveFile(schemeFilter);
@@ -57,10 +58,14 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
candidate = candidate && resources.dirname(candidate);
}
return candidate || undefined;
if (!candidate) {
candidate = await this.pathService.userHome({ preferLocal: schemeFilter === Schemas.file });
}
return candidate;
}
defaultFolderPath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
async defaultFolderPath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> {
// Check for last active file root first...
let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
@@ -70,10 +75,14 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
candidate = this.historyService.getLastActiveFile(schemeFilter);
}
return candidate && resources.dirname(candidate) || undefined;
if (!candidate) {
return this.pathService.userHome({ preferLocal: schemeFilter === Schemas.file });
} else {
return resources.dirname(candidate);
}
}
defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow(), filename?: string): URI | undefined {
async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow(), filename?: string): Promise<URI> {
let defaultWorkspacePath: URI | undefined;
// Check for current workspace config file first...
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
@@ -85,7 +94,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
// ...then fallback to default file path
if (!defaultWorkspacePath) {
defaultWorkspacePath = this.defaultFilePath(schemeFilter);
defaultWorkspacePath = await this.defaultFilePath(schemeFilter);
}
if (defaultWorkspacePath && filename) {
@@ -155,7 +164,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
if (stat.isDirectory || options.forceNewWindow || preferNewWindow) {
return this.hostService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow });
} else {
return this.openerService.open(uri, { fromUserGesture: true });
return this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } });
}
}
}
@@ -172,7 +181,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
if (options.forceNewWindow || preferNewWindow) {
return this.hostService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow });
} else {
return this.openerService.open(uri, { fromUserGesture: true });
return this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } });
}
}
}

View File

@@ -1,149 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IDialogService, IDialogOptions, IConfirmation, IConfirmationResult, DialogType, IShowResult, IInputResult, ICheckbox, IInput } from 'vs/platform/dialogs/common/dialogs';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import Severity from 'vs/base/common/severity';
import { Dialog, IDialogResult } from 'vs/base/browser/ui/dialog/dialog';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachDialogStyler } from 'vs/platform/theme/common/styler';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { EventHelper } from 'vs/base/browser/dom';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IProductService } from 'vs/platform/product/common/productService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { fromNow } from 'vs/base/common/date';
export class DialogService implements IDialogService {
declare readonly _serviceBrand: undefined;
private static readonly ALLOWABLE_COMMANDS = [
'copy',
'cut',
'editor.action.selectAll',
'editor.action.clipboardCopyAction',
'editor.action.clipboardCutAction',
'editor.action.clipboardPasteAction'
];
constructor(
@ILogService private readonly logService: ILogService,
@ILayoutService private readonly layoutService: ILayoutService,
@IThemeService private readonly themeService: IThemeService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IProductService private readonly productService: IProductService,
@IClipboardService private readonly clipboardService: IClipboardService
) { }
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
this.logService.trace('DialogService#confirm', confirmation.message);
const buttons: string[] = [];
if (confirmation.primaryButton) {
buttons.push(confirmation.primaryButton);
} else {
buttons.push(nls.localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
}
if (confirmation.secondaryButton) {
buttons.push(confirmation.secondaryButton);
} else if (typeof confirmation.secondaryButton === 'undefined') {
buttons.push(nls.localize('cancelButton', "Cancel"));
}
const result = await this.doShow(confirmation.type, confirmation.message, buttons, confirmation.detail, 1, confirmation.checkbox);
return { confirmed: result.button === 0, checkboxChecked: result.checkboxChecked };
}
private getDialogType(severity: Severity): DialogType {
return (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none';
}
async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> {
this.logService.trace('DialogService#show', message);
const result = await this.doShow(this.getDialogType(severity), message, buttons, options?.detail, options?.cancelId, options?.checkbox);
return {
choice: result.button,
checkboxChecked: result.checkboxChecked
};
}
private async doShow(type: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | undefined, message: string, buttons: string[], detail?: string, cancelId?: number, checkbox?: ICheckbox, inputs?: IInput[]): Promise<IDialogResult> {
const dialogDisposables = new DisposableStore();
const dialog = new Dialog(
this.layoutService.container,
message,
buttons,
{
detail,
cancelId,
type,
keyEventProcessor: (event: StandardKeyboardEvent) => {
const resolved = this.keybindingService.softDispatch(event, this.layoutService.container);
if (resolved && resolved.commandId) {
if (DialogService.ALLOWABLE_COMMANDS.indexOf(resolved.commandId) === -1) {
EventHelper.stop(event, true);
}
}
},
checkboxLabel: checkbox?.label,
checkboxChecked: checkbox?.checked,
inputs
});
dialogDisposables.add(dialog);
dialogDisposables.add(attachDialogStyler(dialog, this.themeService));
const result = await dialog.show();
dialogDisposables.dispose();
return result;
}
async input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> {
this.logService.trace('DialogService#input', message);
const result = await this.doShow(this.getDialogType(severity), message, buttons, options?.detail, options?.cancelId, options?.checkbox, inputs);
return {
choice: result.button,
checkboxChecked: result.checkboxChecked,
values: result.values
};
}
async about(): Promise<void> {
const detailString = (useAgo: boolean): string => {
return nls.localize('aboutDetail',
"code-server: v{4}\n VS Code: v{0}\nCommit: {1}\nDate: {2}\nBrowser: {3}",
this.productService.version || 'Unknown',
this.productService.commit || 'Unknown',
this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown',
navigator.userAgent,
this.productService.codeServerVersion || 'Unknown',
);
};
const detail = detailString(true);
const detailToCopy = detailString(false);
const { choice } = await this.show(Severity.Info, this.productService.nameLong, [nls.localize('copy', "Copy"), nls.localize('ok', "OK")], { detail, cancelId: 1 });
if (choice === 0) {
this.clipboardService.writeText(detailToCopy);
}
}
}
registerSingleton(IDialogService, DialogService, true);

View File

@@ -15,7 +15,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultFilePath(schema);
options.defaultUri = await this.defaultFilePath(schema);
}
return this.pickFileFolderAndOpenSimplified(schema, options, false);
@@ -25,7 +25,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultFilePath(schema);
options.defaultUri = await this.defaultFilePath(schema);
}
return this.pickFileAndOpenSimplified(schema, options, false);
@@ -35,7 +35,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultFolderPath(schema);
options.defaultUri = await this.defaultFolderPath(schema);
}
return this.pickFolderAndOpenSimplified(schema, options);
@@ -45,7 +45,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultWorkspacePath(schema);
options.defaultUri = await this.defaultWorkspacePath(schema);
}
return this.pickWorkspaceAndOpenSimplified(schema, options);

View File

@@ -212,11 +212,16 @@ export class SimpleFileDialog {
path = path.replace(/\\/g, '/');
}
const uri: URI = this.scheme === Schemas.file ? URI.file(path) : URI.from({ scheme: this.scheme, path });
return resources.toLocalResource(uri, uri.scheme === Schemas.file ? undefined : this.remoteAuthority, this.pathService.defaultUriScheme);
return resources.toLocalResource(uri,
// If the default scheme is file, then we don't care about the remote authority
uri.scheme === Schemas.file ? undefined : this.remoteAuthority,
// If there is a remote authority, then we should use the system's default URI as the local scheme.
// If there is *no* remote authority, then we should use the default scheme for this dialog as that is already local.
this.remoteAuthority ? this.pathService.defaultUriScheme : uri.scheme);
}
private getScheme(available: readonly string[] | undefined, defaultUri: URI | undefined): string {
if (available) {
if (available && available.length > 0) {
if (defaultUri && (available.indexOf(defaultUri.scheme) >= 0)) {
return defaultUri.scheme;
}
@@ -919,7 +924,8 @@ export class SimpleFileDialog {
const ext = resources.extname(file);
for (let i = 0; i < this.options.filters.length; i++) {
for (let j = 0; j < this.options.filters[i].extensions.length; j++) {
if (ext === ('.' + this.options.filters[i].extensions[j])) {
const testExt = this.options.filters[i].extensions[j];
if ((testExt === '*') || (ext === ('.' + testExt))) {
return true;
}
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Severity from 'vs/base/common/severity';
import { Disposable } from 'vs/base/common/lifecycle';
import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInput, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs';
import { DialogsModel, IDialogsModel } from 'vs/workbench/common/dialogs';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class DialogService extends Disposable implements IDialogService {
_serviceBrand: undefined;
readonly model: IDialogsModel = this._register(new DialogsModel());
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
const handle = this.model.show({ confirmArgs: { confirmation } });
return await handle.result as IConfirmationResult;
}
async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> {
const handle = this.model.show({ showArgs: { severity, message, buttons, options } });
return await handle.result as IShowResult;
}
async input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> {
const handle = this.model.show({ inputArgs: { severity, message, buttons, inputs, options } });
return await handle.result as IInputResult;
}
async about(): Promise<void> {
const handle = this.model.show({});
await handle.result;
}
}
registerSingleton(IDialogService, DialogService, true);

View File

@@ -1,268 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import Severity from 'vs/base/common/severity';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions, IShowResult, IInputResult, IInput } from 'vs/platform/dialogs/common/dialogs';
import { DialogService as HTMLDialogService } from 'vs/workbench/services/dialogs/browser/dialogService';
import { ILogService } from 'vs/platform/log/common/log';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IProductService } from 'vs/platform/product/common/productService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { MessageBoxOptions } from 'vs/base/parts/sandbox/common/electronTypes';
import { fromNow } from 'vs/base/common/date';
import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
interface IMassagedMessageBoxOptions {
/**
* OS massaged message box options.
*/
options: MessageBoxOptions;
/**
* Since the massaged result of the message box options potentially
* changes the order of buttons, we have to keep a map of these
* changes so that we can still return the correct index to the caller.
*/
buttonIndexMap: number[];
}
export class DialogService implements IDialogService {
declare readonly _serviceBrand: undefined;
private nativeImpl: IDialogService;
private customImpl: IDialogService;
constructor(
@IConfigurationService private configurationService: IConfigurationService,
@ILogService logService: ILogService,
@ILayoutService layoutService: ILayoutService,
@IThemeService themeService: IThemeService,
@IKeybindingService keybindingService: IKeybindingService,
@IProductService productService: IProductService,
@IClipboardService clipboardService: IClipboardService,
@INativeHostService nativeHostService: INativeHostService
) {
this.customImpl = new HTMLDialogService(logService, layoutService, themeService, keybindingService, productService, clipboardService);
this.nativeImpl = new NativeDialogService(logService, nativeHostService, productService, clipboardService);
}
private get useCustomDialog(): boolean {
return this.configurationService.getValue('window.dialogStyle') === 'custom';
}
confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
if (this.useCustomDialog) {
return this.customImpl.confirm(confirmation);
}
return this.nativeImpl.confirm(confirmation);
}
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult> {
if (this.useCustomDialog) {
return this.customImpl.show(severity, message, buttons, options);
}
return this.nativeImpl.show(severity, message, buttons, options);
}
input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> {
return this.customImpl.input(severity, message, buttons, inputs, options);
}
about(): Promise<void> {
return this.nativeImpl.about();
}
}
class NativeDialogService implements IDialogService {
declare readonly _serviceBrand: undefined;
constructor(
@ILogService private readonly logService: ILogService,
@INativeHostService private readonly nativeHostService: INativeHostService,
@IProductService private readonly productService: IProductService,
@IClipboardService private readonly clipboardService: IClipboardService
) {
}
async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
this.logService.trace('DialogService#confirm', confirmation.message);
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
const result = await this.nativeHostService.showMessageBox(options);
return {
confirmed: buttonIndexMap[result.response] === 0 ? true : false,
checkboxChecked: result.checkboxChecked
};
}
private getConfirmOptions(confirmation: IConfirmation): MessageBoxOptions {
const buttons: string[] = [];
if (confirmation.primaryButton) {
buttons.push(confirmation.primaryButton);
} else {
buttons.push(nls.localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
}
if (confirmation.secondaryButton) {
buttons.push(confirmation.secondaryButton);
} else if (typeof confirmation.secondaryButton === 'undefined') {
buttons.push(nls.localize('cancelButton', "Cancel"));
}
const opts: MessageBoxOptions = {
title: confirmation.title,
message: confirmation.message,
buttons,
cancelId: 1
};
if (confirmation.detail) {
opts.detail = confirmation.detail;
}
if (confirmation.type) {
opts.type = confirmation.type;
}
if (confirmation.checkbox) {
opts.checkboxLabel = confirmation.checkbox.label;
opts.checkboxChecked = confirmation.checkbox.checked;
}
return opts;
}
async show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise<IShowResult> {
this.logService.trace('DialogService#show', message);
const { options, buttonIndexMap } = this.massageMessageBoxOptions({
message,
buttons,
type: (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none',
cancelId: dialogOptions ? dialogOptions.cancelId : undefined,
detail: dialogOptions ? dialogOptions.detail : undefined,
checkboxLabel: dialogOptions && dialogOptions.checkbox ? dialogOptions.checkbox.label : undefined,
checkboxChecked: dialogOptions && dialogOptions.checkbox ? dialogOptions.checkbox.checked : undefined
});
const result = await this.nativeHostService.showMessageBox(options);
return { choice: buttonIndexMap[result.response], checkboxChecked: result.checkboxChecked };
}
private massageMessageBoxOptions(options: MessageBoxOptions): IMassagedMessageBoxOptions {
let buttonIndexMap = (options.buttons || []).map((button, index) => index);
let buttons = (options.buttons || []).map(button => mnemonicButtonLabel(button));
let cancelId = options.cancelId;
// Linux: order of buttons is reverse
// macOS: also reverse, but the OS handles this for us!
if (isLinux) {
buttons = buttons.reverse();
buttonIndexMap = buttonIndexMap.reverse();
}
// Default Button (always first one)
options.defaultId = buttonIndexMap[0];
// Cancel Button
if (typeof cancelId === 'number') {
// Ensure the cancelId is the correct one from our mapping
cancelId = buttonIndexMap[cancelId];
// macOS/Linux: the cancel button should always be to the left of the primary action
// if we see more than 2 buttons, move the cancel one to the left of the primary
if (!isWindows && buttons.length > 2 && cancelId !== 1) {
const cancelButton = buttons[cancelId];
buttons.splice(cancelId, 1);
buttons.splice(1, 0, cancelButton);
const cancelButtonIndex = buttonIndexMap[cancelId];
buttonIndexMap.splice(cancelId, 1);
buttonIndexMap.splice(1, 0, cancelButtonIndex);
cancelId = 1;
}
}
options.buttons = buttons;
options.cancelId = cancelId;
options.noLink = true;
options.title = options.title || this.productService.nameLong;
return { options, buttonIndexMap };
}
input(): never {
throw new Error('Unsupported'); // we have no native API for password dialogs in Electron
}
async about(): Promise<void> {
let version = this.productService.version;
if (this.productService.target) {
version = `${version} (${this.productService.target} setup)`;
}
const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION;
const osProps = await this.nativeHostService.getOSProperties();
const detailString = (useAgo: boolean): string => {
return nls.localize({ key: 'aboutDetail', comment: ['Electron, Chrome, Node.js and V8 are product names that need no translation'] },
"Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}",
version,
this.productService.commit || 'Unknown',
this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown',
process.versions['electron'],
process.versions['chrome'],
process.versions['node'],
process.versions['v8'],
`${osProps.type} ${osProps.arch} ${osProps.release}${isSnap ? ' snap' : ''}`
);
};
const detail = detailString(true);
const detailToCopy = detailString(false);
const ok = nls.localize('okButton', "OK");
const copy = mnemonicButtonLabel(nls.localize({ key: 'copy', comment: ['&& denotes a mnemonic'] }, "&&Copy"));
let buttons: string[];
if (isLinux) {
buttons = [copy, ok];
} else {
buttons = [ok, copy];
}
const result = await this.nativeHostService.showMessageBox({
title: this.productService.nameLong,
type: 'info',
message: this.productService.nameLong,
detail: `\n${detail}`,
buttons,
noLink: true,
defaultId: buttons.indexOf(ok),
cancelId: buttons.indexOf(ok)
});
if (buttons[result.response] === copy) {
this.clipboardService.writeText(detailToCopy);
}
}
}
registerSingleton(IDialogService, DialogService, true);

View File

@@ -68,7 +68,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultFilePath(schema);
options.defaultUri = await this.defaultFilePath(schema);
}
const shouldUseSimplified = this.shouldUseSimplified(schema);
@@ -82,7 +82,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultFilePath(schema);
options.defaultUri = await this.defaultFilePath(schema);
}
const shouldUseSimplified = this.shouldUseSimplified(schema);
@@ -96,7 +96,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultFolderPath(schema);
options.defaultUri = await this.defaultFolderPath(schema);
}
if (this.shouldUseSimplified(schema).useSimplified) {
@@ -109,7 +109,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
if (!options.defaultUri) {
options.defaultUri = this.defaultWorkspacePath(schema);
options.defaultUri = await this.defaultWorkspacePath(schema);
}
if (this.shouldUseSimplified(schema).useSimplified) {

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICodeEditor, isCodeEditor, isDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor, isCodeEditor, isDiffEditor, isCompositeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServiceImpl';
import { ScrollType } from 'vs/editor/common/editorCommon';
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
@@ -69,12 +69,34 @@ export class CodeEditorService extends CodeEditorServiceImpl {
}
private async doOpenCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
// Special case: we want to detect the request to open an editor that
// is different from the current one to decide wether the current editor
// should be pinned or not. This ensures that the source of a navigation
// is not being replaced by the target. An example is "Goto definition"
// that otherwise would replace the editor everytime the user navigates.
if (
source && // we need to know the origin of the navigation
!input.options?.pinned && // we only need to look at preview editors that open
!sideBySide && // we only need to care if editor opens in same group
!isEqual(source.getModel()?.uri, input.resource) // we only need to do this if the editor is about to change
) {
for (const visiblePane of this.editorService.visibleEditorPanes) {
if (getCodeEditor(visiblePane.getControl()) === source) {
visiblePane.group.pinEditor();
break;
}
}
}
// Open as editor
const control = await this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
if (control) {
const widget = control.getControl();
if (isCodeEditor(widget)) {
return widget;
}
if (isCompositeEditor(widget) && isCodeEditor(widget.activeCodeEditor)) {
return widget.activeCodeEditor;
}

View File

@@ -24,7 +24,6 @@ import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from
import { coalesce, distinct, insert } from 'vs/base/common/arrays';
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { ILabelService } from 'vs/platform/label/common/label';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
@@ -73,7 +72,6 @@ export class EditorService extends Disposable implements EditorServiceImpl {
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILabelService private readonly labelService: ILabelService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@@ -817,11 +815,12 @@ export class EditorService extends Disposable implements EditorServiceImpl {
const leftInput = this.createEditorInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile });
const rightInput = this.createEditorInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile });
return new DiffEditorInput(
resourceDiffInput.label || this.toSideBySideLabel(leftInput, rightInput),
return this.instantiationService.createInstance(DiffEditorInput,
resourceDiffInput.label,
resourceDiffInput.description,
leftInput,
rightInput
rightInput,
undefined
);
}
@@ -881,7 +880,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// File
if (resourceEditorInput.forceFile || this.fileService.canHandleResource(canonicalResource)) {
return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService);
return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, preferredResource, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService);
}
// Resource
@@ -897,6 +896,14 @@ export class EditorService extends Disposable implements EditorServiceImpl {
else if (!(cachedInput instanceof ResourceEditorInput)) {
cachedInput.setPreferredResource(preferredResource);
if (resourceEditorInput.label) {
cachedInput.setPreferredName(resourceEditorInput.label);
}
if (resourceEditorInput.description) {
cachedInput.setPreferredDescription(resourceEditorInput.description);
}
if (resourceEditorInput.encoding) {
cachedInput.setPreferredEncoding(resourceEditorInput.encoding);
}
@@ -968,19 +975,6 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return input;
}
private toSideBySideLabel(leftInput: EditorInput, rightInput: EditorInput): string | undefined {
// If both editors are file inputs, we produce an optimized label
// by adding the relative path of both inputs to the label. This
// makes it easier to understand a file-based comparison.
if (this.fileEditorInputFactory.isFileEditorInput(leftInput) && this.fileEditorInputFactory.isFileEditorInput(rightInput)) {
return `${this.labelService.getUriLabel(leftInput.preferredResource, { relative: true })} ↔ ${this.labelService.getUriLabel(rightInput.preferredResource, { relative: true })}`;
}
// Signal back that the label should be computed from within the editor
return undefined;
}
//#endregion
//#region save/revert

View File

@@ -21,16 +21,6 @@ export const enum GroupDirection {
RIGHT
}
export function preferredSideBySideGroupDirection(configurationService: IConfigurationService): GroupDirection.DOWN | GroupDirection.RIGHT {
const openSideBySideDirection = configurationService.getValue<'right' | 'down'>('workbench.editor.openSideBySideDirection');
if (openSideBySideDirection === 'down') {
return GroupDirection.DOWN;
}
return GroupDirection.RIGHT;
}
export const enum GroupOrientation {
HORIZONTAL,
VERTICAL
@@ -595,3 +585,18 @@ export interface IEditorGroup {
*/
focus(): void;
}
//#region Editor Group Helpers
export function preferredSideBySideGroupDirection(configurationService: IConfigurationService): GroupDirection.DOWN | GroupDirection.RIGHT {
const openSideBySideDirection = configurationService.getValue<'right' | 'down'>('workbench.editor.openSideBySideDirection');
if (openSideBySideDirection === 'down') {
return GroupDirection.DOWN;
}
return GroupDirection.RIGHT;
}
//#endregion

View File

@@ -16,6 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { URI } from 'vs/base/common/uri';
import { extname, basename, isEqual } from 'vs/base/common/resources';
import { Codicon } from 'vs/base/common/codicons';
/**
* Id of the default editor for open with.
@@ -71,7 +72,7 @@ export async function openEditorWith(
description: entry.active ? nls.localize('promptOpenWith.currentlyActive', 'Currently Active') : undefined,
detail: entry.detail,
buttons: resourceExt ? [{
iconClass: 'codicon-settings-gear',
iconClass: Codicon.gear.classNames,
tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt)
}] : undefined
};

View File

@@ -298,7 +298,7 @@ suite('EditorsObserver', function () {
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
storage.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;
@@ -350,7 +350,7 @@ suite('EditorsObserver', function () {
assert.equal(observer.hasEditor(input2.resource), true);
assert.equal(observer.hasEditor(input3.resource), true);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
storage.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;
@@ -390,7 +390,7 @@ suite('EditorsObserver', function () {
assert.equal(currentEditorsMRU[0].editor, input1);
assert.equal(observer.hasEditor(input1.resource), true);
storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
storage.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
const restoredObserver = new EditorsObserver(part, storage);
await part.whenRestored;

View File

@@ -15,6 +15,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { memoize } from 'vs/base/common/decorators';
import { onUnexpectedError } from 'vs/base/common/errors';
import { parseLineAndColumnAware } from 'vs/base/common/extpath';
import { LogLevelToString } from 'vs/platform/log/common/log';
class BrowserWorkbenchConfiguration implements IWindowConfiguration {
@@ -114,7 +115,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
@memoize
get logsPath(): string { return this.options.logsPath.path; }
get logLevel(): string | undefined { return this.payload?.get('logLevel'); }
@memoize
get logLevel(): string | undefined { return this.payload?.get('logLevel') || (this.options.logLevel !== undefined ? LogLevelToString(this.options.logLevel) : undefined); }
@memoize
get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); }
@@ -174,9 +176,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
@memoize
get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get backupWorkspaceHome(): URI { return joinPath(this.userRoamingDataHome, 'Backups', this.options.workspaceId); }
@memoize
get untitledWorkspacesHome(): URI { return joinPath(this.userRoamingDataHome, 'Workspaces'); }

View File

@@ -34,7 +34,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService {
readonly sessionId: string;
readonly logFile: URI;
readonly backupWorkspaceHome?: URI;
readonly extHostLogsPath: URI;
readonly logExtensionHostCommunication?: boolean;

View File

@@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { join } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { IOSConfiguration } from 'vs/platform/windows/common/windows';
export class NativeWorkbenchEnvironmentService extends NativeEnvironmentService implements INativeWorkbenchEnvironmentService {
@@ -30,20 +31,6 @@ export class NativeWorkbenchEnvironmentService extends NativeEnvironmentService
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }
// Do NOT! memoize as `backupPath` can change in configuration
// via the `updateBackupPath` method below
get backupWorkspaceHome(): URI | undefined {
if (this.configuration.backupPath) {
return URI.file(this.configuration.backupPath).with({ scheme: this.userRoamingDataHome.scheme });
}
return undefined;
}
updateBackupPath(newBackupPath: string | undefined): void {
this.configuration.backupPath = newBackupPath;
}
@memoize
get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.configuration.windowId}.log`)); }
@@ -82,6 +69,10 @@ export class NativeWorkbenchEnvironmentService extends NativeEnvironmentService
return undefined;
}
get os(): IOSConfiguration {
return this.configuration.os;
}
constructor(
readonly configuration: INativeWorkbenchConfiguration,
private readonly productService: IProductService

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchConfiguration, IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWindowConfiguration } from 'vs/platform/windows/common/windows';
import { INativeWindowConfiguration, IOSConfiguration } from 'vs/platform/windows/common/windows';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -27,8 +27,7 @@ export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmen
readonly log?: string;
// TODO@ben this is a bit ugly
updateBackupPath(newPath: string | undefined): void;
readonly os: IOSConfiguration;
/**
* @deprecated this property will go away eventually as it

View File

@@ -6,18 +6,19 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as platform from 'vs/base/common/platform';
import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client-umd';
import { MementoObject, Memento } from 'vs/workbench/common/memento';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryData } from 'vs/base/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import product from 'vs/platform/product/common/product';
export const ITASExperimentService = createDecorator<ITASExperimentService>('TASExperimentService');
export interface ITASExperimentService {
readonly _serviceBrand: undefined;
getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined>;
getCurrentExperiments(): Promise<string[] | undefined>;
}
const storageKey = 'VSCode.ABExp.FeatureData';
@@ -37,11 +38,20 @@ class MementoKeyValueStorage implements IKeyValueStorage {
}
class ExperimentServiceTelemetry implements IExperimentationTelemetry {
private _lastAssignmentContext: string | undefined;
constructor(private telemetryService: ITelemetryService) { }
get assignmentContext(): string[] | undefined {
return this._lastAssignmentContext?.split(';');
}
// __GDPR__COMMON__ "VSCode.ABExp.Features" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
setSharedProperty(name: string, value: string): void {
if (name === product.tasConfig?.assignmentContextTelemetryPropertyName) {
this._lastAssignmentContext = value;
}
this.telemetryService.setExperimentProperty(name, value);
}
@@ -165,6 +175,7 @@ enum TargetPopulation {
export class ExperimentService implements ITASExperimentService {
_serviceBrand: undefined;
private tasClient: Promise<TASClient> | undefined;
private telemetry: ExperimentServiceTelemetry | undefined;
private static MEMENTO_ID = 'experiment.service.memento';
private get experimentsEnabled(): boolean {
@@ -172,13 +183,12 @@ export class ExperimentService implements ITASExperimentService {
}
constructor(
@IProductService private productService: IProductService,
@ITelemetryService private telemetryService: ITelemetryService,
@IStorageService private storageService: IStorageService,
@IConfigurationService private configurationService: IConfigurationService,
) {
if (this.productService.tasConfig && this.experimentsEnabled && this.telemetryService.isOptedIn) {
if (product.tasConfig && this.experimentsEnabled && this.telemetryService.isOptedIn) {
this.tasClient = this.setupTASClient();
}
}
@@ -195,26 +205,40 @@ export class ExperimentService implements ITASExperimentService {
return (await this.tasClient).getTreatmentVariable<T>('vscode', name);
}
async getCurrentExperiments(): Promise<string[] | undefined> {
if (!this.tasClient) {
return undefined;
}
if (!this.experimentsEnabled) {
return undefined;
}
await this.tasClient;
return this.telemetry?.assignmentContext;
}
private async setupTASClient(): Promise<TASClient> {
const telemetryInfo = await this.telemetryService.getTelemetryInfo();
const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (this.productService.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders);
const targetPopulation = telemetryInfo.msftInternal ? TargetPopulation.Internal : (product.quality === 'stable' ? TargetPopulation.Public : TargetPopulation.Insiders);
const machineId = telemetryInfo.machineId;
const filterProvider = new ExperimentServiceFilterProvider(
this.productService.version,
this.productService.nameLong,
product.version,
product.nameLong,
machineId,
targetPopulation
);
const memento = new Memento(ExperimentService.MEMENTO_ID, this.storageService);
const keyValueStorage = new MementoKeyValueStorage(memento.getMemento(StorageScope.GLOBAL));
const keyValueStorage = new MementoKeyValueStorage(memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE));
const telemetry = new ExperimentServiceTelemetry(this.telemetryService);
this.telemetry = new ExperimentServiceTelemetry(this.telemetryService);
const tasConfig = this.productService.tasConfig!;
const tasConfig = product.tasConfig!;
const tasClient = new (await import('tas-client-umd')).ExperimentationService({
filterProviders: [filterProvider],
telemetry: telemetry,
telemetry: this.telemetry,
storageKey: storageKey,
keyValueStorage: keyValueStorage,
featuresTelemetryPropertyName: tasConfig.featuresTelemetryPropertyName,

View File

@@ -0,0 +1,337 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue';
import { timeout } from 'vs/base/common/async';
// --- bisect service
export const IExtensionBisectService = createDecorator<IExtensionBisectService>('IExtensionBisectService');
export interface IExtensionBisectService {
readonly _serviceBrand: undefined;
isDisabledByBisect(extension: IExtension): boolean;
isActive: boolean;
disabledCount: number;
start(extensions: ILocalExtension[]): Promise<void>;
next(seeingBad: boolean): Promise<{ id: string, bad: boolean } | undefined>;
reset(): Promise<void>;
}
class BisectState {
static fromJSON(raw: string | undefined): BisectState | undefined {
if (!raw) {
return undefined;
}
try {
interface Raw extends BisectState { }
const data: Raw = JSON.parse(raw);
return new BisectState(data.extensions, data.low, data.high, data.mid);
} catch {
return undefined;
}
}
constructor(
readonly extensions: string[],
readonly low: number,
readonly high: number,
readonly mid: number = ((low + high) / 2) | 0
) { }
}
class ExtensionBisectService implements IExtensionBisectService {
declare readonly _serviceBrand: undefined;
private static readonly _storageKey = 'extensionBisectState';
private readonly _state: BisectState | undefined;
private readonly _disabled = new Map<string, boolean>();
constructor(
@ILogService logService: ILogService,
@IStorageService private readonly _storageService: IStorageService,
) {
const raw = _storageService.get(ExtensionBisectService._storageKey, StorageScope.GLOBAL);
this._state = BisectState.fromJSON(raw);
if (this._state) {
const { mid, high } = this._state;
for (let i = 0; i < this._state.extensions.length; i++) {
const isDisabled = i >= mid && i < high;
this._disabled.set(this._state.extensions[i], isDisabled);
}
logService.warn('extension BISECT active', [...this._disabled]);
}
}
get isActive() {
return !!this._state;
}
get disabledCount() {
return this._state ? this._state.high - this._state.mid : -1;
}
isDisabledByBisect(extension: IExtension): boolean {
if (!this._state) {
return false;
}
const disabled = this._disabled.get(extension.identifier.id);
return disabled ?? false;
}
async start(extensions: ILocalExtension[]): Promise<void> {
if (this._state) {
throw new Error('invalid state');
}
const extensionIds = extensions.map(ext => ext.identifier.id);
const newState = new BisectState(extensionIds, 0, extensionIds.length, 0);
this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.GLOBAL, StorageTarget.MACHINE);
await this._storageService.flush();
}
async next(seeingBad: boolean): Promise<{ id: string; bad: boolean; } | undefined> {
if (!this._state) {
throw new Error('invalid state');
}
// check if bad when all extensions are disabled
if (seeingBad && this._state.mid === 0 && this._state.high === this._state.extensions.length) {
return { bad: true, id: '' };
}
// check if there is only one left
if (this._state.low === this._state.high - 1) {
await this.reset();
return { id: this._state.extensions[this._state.low], bad: seeingBad };
}
// the second half is disabled so if there is still bad it must be
// in the first half
const nextState = new BisectState(
this._state.extensions,
seeingBad ? this._state.low : this._state.mid,
seeingBad ? this._state.mid : this._state.high,
);
this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(nextState), StorageScope.GLOBAL, StorageTarget.MACHINE);
await this._storageService.flush();
return undefined;
}
async reset(): Promise<void> {
this._storageService.remove(ExtensionBisectService._storageKey, StorageScope.GLOBAL);
await this._storageService.flush();
}
}
registerSingleton(IExtensionBisectService, ExtensionBisectService, true);
// --- bisect UI
class ExtensionBisectUi {
static ctxIsBisectActive = new RawContextKey('isExtensionBisectActive', false);
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IExtensionBisectService private readonly _extensionBisectService: IExtensionBisectService,
@INotificationService private readonly _notificationService: INotificationService,
@ICommandService private readonly _commandService: ICommandService,
) {
if (_extensionBisectService.isActive) {
ExtensionBisectUi.ctxIsBisectActive.bindTo(contextKeyService).set(true);
this._showBisectPrompt();
}
}
private _showBisectPrompt(): void {
const goodPrompt: IPromptChoice = {
label: 'Good now',
run: () => this._commandService.executeCommand('extension.bisect.next', false)
};
const badPrompt: IPromptChoice = {
label: 'This is bad',
run: () => this._commandService.executeCommand('extension.bisect.next', true)
};
const stop: IPromptChoice = {
label: 'Stop Bisect',
run: () => this._commandService.executeCommand('extension.bisect.stop')
};
this._notificationService.prompt(
Severity.Info,
localize('bisect', "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", this._extensionBisectService.disabledCount),
[goodPrompt, badPrompt, stop],
{ sticky: true }
);
}
}
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench).registerWorkbenchContribution(
ExtensionBisectUi,
LifecyclePhase.Restored
);
registerAction2(class extends Action2 {
constructor() {
super({
id: 'extension.bisect.start',
title: localize('title.start', "Start Extension Bisect"),
category: localize('help', "Help"),
f1: true,
precondition: ExtensionBisectUi.ctxIsBisectActive.negate()
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const dialogService = accessor.get(IDialogService);
const hostService = accessor.get(IHostService);
const extensionManagement = accessor.get(IExtensionManagementService);
const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
const extensionsBisect = accessor.get(IExtensionBisectService);
const disabled = new Set(extensionEnablementService.getDisabledExtensions().map(id => id.id));
const extensions = (await extensionManagement.getInstalled(ExtensionType.User)).filter(ext => !disabled.has(ext.identifier.id));
const res = await dialogService.confirm({
message: localize('msg.start', "Extension Bisect"),
detail: localize('detail.start', "Extension Bisect will use binary search to find an extension that causes a problem. During the process the window reloads repeatedly (~{0} times). Each time you must confirm if you are still seeing problems.", 2 + Math.log2(extensions.length) | 0),
primaryButton: localize('msg2', "Start Extension Bisect")
});
if (res.confirmed) {
await extensionsBisect.start(extensions);
hostService.reload();
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'extension.bisect.next',
title: localize('title.isBad', "Continue Extension Bisect"),
category: localize('help', "Help"),
f1: true,
precondition: ExtensionBisectUi.ctxIsBisectActive
});
}
async run(accessor: ServicesAccessor, seeingBad: boolean | undefined): Promise<void> {
const dialogService = accessor.get(IDialogService);
const hostService = accessor.get(IHostService);
const bisectService = accessor.get(IExtensionBisectService);
const productService = accessor.get(IProductService);
const extensionEnablementService = accessor.get(IGlobalExtensionEnablementService);
const issueService = accessor.get(IWorkbenchIssueService);
if (!bisectService.isActive) {
return;
}
if (seeingBad === undefined) {
const goodBadStopCancel = await this._checkForBad(dialogService, bisectService);
if (goodBadStopCancel === null) {
return;
}
seeingBad = goodBadStopCancel;
}
if (seeingBad === undefined) {
await bisectService.reset();
hostService.reload();
return;
}
const done = await bisectService.next(seeingBad);
if (!done) {
hostService.reload();
return;
}
if (done.bad) {
// DONE but nothing found
await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [], {
detail: localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", productService.nameShort)
});
} else {
// DONE and identified extension
const res = await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"),
[localize('report', "Report Issue & Continue"), localize('done', "Continue")],
// [],
{
detail: localize('done.detail', "Extension Bisect is done and has identified {0} as the extension causing the problem.", done.id),
checkbox: { label: localize('done.disbale', "Keep this extension disabled"), checked: true },
cancelId: 1
}
);
if (res.checkboxChecked) {
await extensionEnablementService.disableExtension({ id: done.id }, undefined);
}
if (res.choice === 0) {
issueService.openReporter({ extensionId: done.id });
await timeout(750); // workaround for https://github.com/microsoft/vscode/issues/111871
}
}
await bisectService.reset();
hostService.reload();
}
private async _checkForBad(dialogService: IDialogService, bisectService: IExtensionBisectService): Promise<boolean | undefined | null> {
const options = {
cancelId: 3,
detail: localize('bisect', "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", bisectService.disabledCount),
};
const res = await dialogService.show(
Severity.Info,
localize('msg.next', "Extension Bisect"),
[localize('next.good', "Good now"), localize('next.bad', "This is bad"), localize('next.stop', "Stop Bisect"), localize('next.cancel', "Cancel")],
options
);
switch (res.choice) {
case 0: return false; //good now
case 1: return true; //bad
case 2: return undefined; //stop
}
return null; //cancel
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'extension.bisect.stop',
title: localize('title.stop', "Stop Extension Bisect"),
category: localize('help', "Help"),
f1: true,
precondition: ExtensionBisectUi.ctxIsBisectActive
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const extensionsBisect = accessor.get(IExtensionBisectService);
const hostService = accessor.get(IHostService);
await extensionsBisect.reset();
hostService.reload();
}
});

View File

@@ -24,6 +24,7 @@ import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/com
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect';
const SOURCE = 'IWorkbenchExtensionEnablementService';
@@ -50,6 +51,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@INotificationService private readonly notificationService: INotificationService,
@IHostService hostService: IHostService,
@IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService,
) {
super();
this.storageManger = this._register(new StorageManager(storageService));
@@ -59,9 +61,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
// delay notification for extensions disabled until workbench restored
if (this.allUserExtensionsDisabled) {
this.lifecycleService.when(LifecyclePhase.Restored).then(() => {
this.notificationService.prompt(Severity.Info, localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{
label: localize('Reload', "Reload"),
run: () => hostService.reload()
this.notificationService.prompt(Severity.Info, localize('extensionsDisabled', "All installed extensions are temporarily disabled."), [{
label: localize('Reload', "Reload and Enable Extensions"),
run: () => hostService.reload({ disableExtensions: false })
}]);
});
}
@@ -76,6 +78,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
}
getEnablementState(extension: IExtension): EnablementState {
if (this.extensionBisectService.isDisabledByBisect(extension)) {
return EnablementState.DisabledByEnvironemt;
}
if (this._isDisabledInEnv(extension)) {
return EnablementState.DisabledByEnvironemt;
}
@@ -210,14 +215,16 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
}
}
if (extensionKind === 'web') {
const enableLocalWebWorker = this.configurationService.getValue<boolean>(webWorkerExtHostConfig);
if (enableLocalWebWorker) {
// Web extensions are enabled on all configurations
return false;
}
if (this.extensionManagementServerService.localExtensionManagementServer === null) {
// Web extensions run only in the web
return false;
if (this.extensionManagementServerService.webExtensionManagementServer) {
if (server === this.extensionManagementServerService.webExtensionManagementServer) {
return false;
}
} else if (server === this.extensionManagementServerService.localExtensionManagementServer) {
const enableLocalWebWorker = this.configurationService.getValue<boolean>(webWorkerExtHostConfig);
if (enableLocalWebWorker) {
// Web extensions are enabled on all configurations
return false;
}
}
}
}

View File

@@ -3,12 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IOutputChannelModelService, AsbtractOutputChannelModelService } from 'vs/workbench/services/output/common/outputChannelModel';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class OutputChannelModelService extends AsbtractOutputChannelModelService implements IOutputChannelModelService {
class ExtensionUrlTrustService implements IExtensionUrlTrustService {
declare readonly _serviceBrand: undefined;
async isExtensionUrlTrusted(): Promise<boolean> {
return false;
}
}
registerSingleton(IOutputChannelModelService, OutputChannelModelService);
registerSingleton(IExtensionUrlTrustService, ExtensionUrlTrustService);

View File

@@ -5,7 +5,7 @@
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtension, IScannedExtension, ExtensionType, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions';
import { IExtension, IScannedExtension, ExtensionType, ITranslatedScannedExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { URI } from 'vs/base/common/uri';
@@ -29,6 +29,7 @@ export interface IWorkbenchExtensioManagementService extends IExtensionManagemen
readonly _serviceBrand: undefined;
installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions): Promise<ILocalExtension[]>;
updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension): Promise<ILocalExtension>;
getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null
}
export const enum EnablementState {
@@ -96,7 +97,7 @@ export interface IWebExtensionsScannerService {
scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]>;
scanAndTranslateExtensions(type?: ExtensionType): Promise<ITranslatedScannedExtension[]>;
scanAndTranslateSingleExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<ITranslatedScannedExtension | null>;
canAddExtension(galleryExtension: IGalleryExtension): Promise<boolean>;
canAddExtension(galleryExtension: IGalleryExtension): boolean;
addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension>;
removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>;
}

View File

@@ -43,12 +43,13 @@ export class ExtensionManagementServerService implements IExtensionManagementSer
extensionManagementService,
get label() { return labelService.getHostLabel(Schemas.vscodeRemote, remoteAgentConnection!.remoteAuthority) || localize('remote', "Remote"); }
};
} else if (isWeb) {
}
if (isWeb) {
const extensionManagementService = instantiationService.createInstance(WebExtensionManagementService);
this.webExtensionManagementServer = {
id: 'web',
extensionManagementService,
label: localize('web', "Web")
label: localize('browser', "Browser")
};
}
}

View File

@@ -5,7 +5,7 @@
import { Event, EventMultiplexer } from 'vs/base/common/event';
import {
ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions
ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, UninstallOptions
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServer, IExtensionManagementServerService, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
@@ -15,7 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localize } from 'vs/nls';
import { prefersExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWorkspace, canExecuteOnUI, prefersExecuteOnWeb, canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { prefersExecuteOnUI, getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IProductService } from 'vs/platform/product/common/productService';
import { Schemas } from 'vs/base/common/network';
import { IDownloadService } from 'vs/platform/download/common/download';
@@ -23,7 +23,6 @@ import { flatten } from 'vs/base/common/arrays';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { canceled } from 'vs/base/common/errors';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
export class ExtensionManagementService extends Disposable implements IWorkbenchExtensioManagementService {
@@ -45,7 +44,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
@IDownloadService protected readonly downloadService: IDownloadService,
@IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IDialogService private readonly dialogService: IDialogService,
) {
super();
if (this.extensionManagementServerService.localExtensionManagementServer) {
@@ -69,7 +68,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
return flatten(result);
}
async uninstall(extension: ILocalExtension): Promise<void> {
async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void> {
const server = this.getServer(extension);
if (!server) {
return Promise.reject(`Invalid location ${extension.location.toString()}`);
@@ -78,7 +77,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
if (isLanguagePackExtension(extension.manifest)) {
return this.uninstallEverywhere(extension);
}
return this.uninstallInServer(extension, server);
return this.uninstallInServer(extension, server, options);
}
return server.extensionManagementService.uninstall(extension);
}
@@ -102,7 +101,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
return promise;
}
private async uninstallInServer(extension: ILocalExtension, server: IExtensionManagementServer, force?: boolean): Promise<void> {
private async uninstallInServer(extension: ILocalExtension, server: IExtensionManagementServer, options?: UninstallOptions): Promise<void> {
if (server === this.extensionManagementServerService.localExtensionManagementServer) {
const installedExtensions = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getInstalled(ExtensionType.User);
const dependentNonUIExtensions = installedExtensions.filter(i => !prefersExecuteOnUI(i.manifest, this.productService, this.configurationService)
@@ -111,7 +110,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
return Promise.reject(new Error(this.getDependentsErrorMessage(extension, dependentNonUIExtensions)));
}
}
return server.extensionManagementService.uninstall(extension, force);
return server.extensionManagementService.uninstall(extension, options);
}
private getDependentsErrorMessage(extension: ILocalExtension, dependents: ILocalExtension[]): string {
@@ -285,80 +284,65 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
return Promise.reject(error);
}
private getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | undefined {
getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null {
// Only local server
if (this.servers.length === 1 && this.extensionManagementServerService.localExtensionManagementServer) {
return this.extensionManagementServerService.localExtensionManagementServer;
}
// 1. Install on preferred location
// Install UI preferred extension on local server
if (prefersExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) {
return this.extensionManagementServerService.localExtensionManagementServer;
}
// Install Workspace preferred extension on remote server
if (prefersExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) {
return this.extensionManagementServerService.remoteExtensionManagementServer;
}
// Install Web preferred extension on web server
if (prefersExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) {
return this.extensionManagementServerService.webExtensionManagementServer;
}
// 2. Install on supported location
// Install UI supported extension on local server
if (canExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) {
return this.extensionManagementServerService.localExtensionManagementServer;
}
// Install Workspace supported extension on remote server
if (canExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) {
return this.extensionManagementServerService.remoteExtensionManagementServer;
}
// Install Web supported extension on web server
if (canExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) {
return this.extensionManagementServerService.webExtensionManagementServer;
const extensionKind = getExtensionKind(manifest, this.productService, this.configurationService);
for (const kind of extensionKind) {
if (kind === 'ui' && this.extensionManagementServerService.localExtensionManagementServer) {
return this.extensionManagementServerService.localExtensionManagementServer;
}
if (kind === 'workspace' && this.extensionManagementServerService.remoteExtensionManagementServer) {
return this.extensionManagementServerService.remoteExtensionManagementServer;
}
if (kind === 'web' && this.extensionManagementServerService.webExtensionManagementServer) {
return this.extensionManagementServerService.webExtensionManagementServer;
}
}
<<<<<<< HEAD
// NOTE@coder: Fall back to installing on the remote server.
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
return this.extensionManagementServerService.remoteExtensionManagementServer;
}
return undefined;
=======
// Local server can accept any extension. So return local server if not compatible server found.
return this.extensionManagementServerService.localExtensionManagementServer;
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
}
private async hasToFlagExtensionsMachineScoped(extensions: IGalleryExtension[]): Promise<boolean> {
if (!this.userDataAutoSyncEnablementService.isEnabled() || !this.userDataSyncResourceEnablementService.isResourceEnabled(SyncResource.Extensions)) {
return false;
}
return this.instantiationService.invokeFunction(async accessor => {
const dialogService = accessor.get(IDialogService);
const result = await dialogService.show(
Severity.Info,
extensions.length === 1 ? localize('install extension', "Install Extension") : localize('install extensions', "Install Extensions"),
[
localize('install', "Install"),
localize('install and do no sync', "Install (Do not sync)"),
localize('cancel', "Cancel"),
],
{
cancelId: 2,
detail: extensions.length === 1
? localize('install single extension', "Would you like to install and synchronize '{0}' extension across your devices?", extensions[0].displayName)
: localize('install multiple extensions', "Would you like to install and synchronize extensions across your devices?")
}
);
switch (result.choice) {
case 0:
return false;
case 1:
return true;
const result = await this.dialogService.show(
Severity.Info,
extensions.length === 1 ? localize('install extension', "Install Extension") : localize('install extensions', "Install Extensions"),
[
localize('install', "Install"),
localize('install and do no sync', "Install (Do not sync)"),
localize('cancel', "Cancel"),
],
{
cancelId: 2,
detail: extensions.length === 1
? localize('install single extension', "Would you like to install and synchronize '{0}' extension across your devices?", extensions[0].displayName)
: localize('install multiple extensions', "Would you like to install and synchronize extensions across your devices?")
}
throw canceled();
});
);
switch (result.choice) {
case 0:
return false;
case 1:
return true;
}
throw canceled();
}
getExtensionsReport(): Promise<IReportedExtension[]> {

View File

@@ -25,6 +25,7 @@ import { Event } from 'vs/base/common/event';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { localize } from 'vs/nls';
import * as semver from 'vs/base/common/semver/semver';
import { isArray } from 'vs/base/common/types';
interface IUserExtension {
identifier: IExtensionIdentifier;
@@ -125,25 +126,31 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
private async readDefaultUserWebExtensions(): Promise<IStaticExtension[]> {
const result: IStaticExtension[] = [];
const defaultUserWebExtensions = this.configurationService.getValue<{ location: string }[]>('_extensions.defaultUserWebExtensions') || [];
for (const webExtension of defaultUserWebExtensions) {
const extensionLocation = URI.parse(webExtension.location);
const manifestLocation = joinPath(extensionLocation, 'package.json');
const context = await this.requestService.request({ type: 'GET', url: manifestLocation.toString(true) }, CancellationToken.None);
if (!isSuccess(context)) {
this.logService.warn('Skipped default user web extension as there is an error while fetching manifest', manifestLocation);
continue;
const defaultUserWebExtensions = this.configurationService.getValue<{ location: string }[]>('_extensions.defaultUserWebExtensions');
if (isArray(defaultUserWebExtensions)) {
for (const webExtension of defaultUserWebExtensions) {
try {
const extensionLocation = URI.parse(webExtension.location);
const manifestLocation = joinPath(extensionLocation, 'package.json');
const context = await this.requestService.request({ type: 'GET', url: manifestLocation.toString(true) }, CancellationToken.None);
if (!isSuccess(context)) {
this.logService.warn('Skipped default user web extension as there is an error while fetching manifest', manifestLocation);
continue;
}
const content = await asText(context);
if (!content) {
this.logService.warn('Skipped default user web extension as there is manifest is not found', manifestLocation);
continue;
}
const packageJSON = JSON.parse(content);
result.push({
packageJSON,
extensionLocation,
});
} catch (error) {
this.logService.warn('Skipped default user web extension as there is an error while fetching manifest', webExtension);
}
}
const content = await asText(context);
if (!content) {
this.logService.warn('Skipped default user web extension as there is manifest is not found', manifestLocation);
continue;
}
const packageJSON = JSON.parse(content);
result.push({
packageJSON,
extensionLocation,
});
}
return result;
}
@@ -231,12 +238,12 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
};
}
async canAddExtension(galleryExtension: IGalleryExtension): Promise<boolean> {
canAddExtension(galleryExtension: IGalleryExtension): boolean {
return !!galleryExtension.properties.webExtension && !!galleryExtension.webResource;
}
async addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension> {
if (!(await this.canAddExtension(galleryExtension))) {
if (!this.canAddExtension(galleryExtension)) {
const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", galleryExtension.displayName || galleryExtension.name));
error.name = INSTALL_ERROR_NOT_SUPPORTED;
throw error;

View File

@@ -15,8 +15,8 @@ import { IDownloadService } from 'vs/platform/download/common/download';
import { IProductService } from 'vs/platform/product/common/productService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { joinPath } from 'vs/base/common/resources';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
export class ExtensionManagementService extends BaseExtensionManagementService {
@@ -29,9 +29,9 @@ export class ExtensionManagementService extends BaseExtensionManagementService {
@IDownloadService downloadService: IDownloadService,
@IUserDataAutoSyncEnablementService userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@IInstantiationService instantiationService: IInstantiationService,
@IDialogService dialogService: IDialogService,
) {
super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataAutoSyncEnablementService, userDataSyncResourceEnablementService, instantiationService);
super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataAutoSyncEnablementService, userDataSyncResourceEnablementService, dialogService);
}
protected async installVSIX(vsix: URI, server: IExtensionManagementServer): Promise<ILocalExtension> {

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
class ExtensionUrlTrustService {
declare readonly _serviceBrand: undefined;
constructor(@IMainProcessService mainProcessService: IMainProcessService) {
return createChannelSender<IExtensionUrlTrustService>(mainProcessService.getChannel('extensionUrlTrust'));
}
}
registerSingleton(IExtensionUrlTrustService, ExtensionUrlTrustService);

View File

@@ -28,6 +28,8 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { mock } from 'vs/base/test/common/mock';
import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect';
function createStorageService(instantiationService: TestInstantiationService): IStorageService {
let service = instantiationService.get(IStorageService);
@@ -61,7 +63,8 @@ export class TestExtensionEnablementService extends ExtensionEnablementService {
instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService),
instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, new TestLifecycleService()),
instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()),
instantiationService.get(IHostService)
instantiationService.get(IHostService),
new class extends mock<IExtensionBisectService>() { isDisabledByBisect() { return false; } }
);
}
@@ -542,36 +545,48 @@ suite('ExtensionEnablementService Test', () => {
assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true);
});
test('test web extension on local server is disabled by kind', async () => {
test('test web extension on local server is disabled by kind when web worker is not enabled', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`) });
(<TestConfigurationService>instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(!testObject.isEnabled(localWorkspaceExtension));
assert.equal(testObject.isEnabled(localWorkspaceExtension), false);
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind);
});
test('test web extension on remote server is not disabled by kind when there is no local server', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
test('test web extension on local server is not disabled by kind when web worker is enabled', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`) });
(<TestConfigurationService>instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: true });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.equal(testObject.isEnabled(localWorkspaceExtension), true);
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
});
test('test web extension with no server is not disabled by kind when there is no local server', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) });
test('test web extension on remote server is disabled by kind when web worker is not enabled', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) });
(<TestConfigurationService>instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
assert.equal(testObject.isEnabled(localWorkspaceExtension), false);
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind);
});
test('test web extension with no server is not disabled by kind when there is no local and remote server', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, null, anExtensionManagementServer('web', instantiationService)));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) });
test('test web extension on remote server is disabled by kind when web worker is enabled', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) });
(<TestConfigurationService>instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: true });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
assert.equal(testObject.isEnabled(localWorkspaceExtension), false);
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind);
});
test('test web extension on web server is not disabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
const webExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: 'web' }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.equal(testObject.isEnabled(webExtension), true);
assert.deepEqual(testObject.getEnablementState(webExtension), EnablementState.EnabledGlobally);
});
});
@@ -595,7 +610,7 @@ function anExtensionManagementServerService(localExtensionManagementServer: IExt
_serviceBrand: undefined,
localExtensionManagementServer,
remoteExtensionManagementServer,
webExtensionManagementServer: null,
webExtensionManagementServer,
getExtensionManagementServer: (extension: IExtension) => {
if (extension.location.scheme === Schemas.file) {
return localExtensionManagementServer;

View File

@@ -7,8 +7,7 @@ import { distinct } from 'vs/base/common/arrays';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IExtensionIgnoredRecommendationsService, IgnoredRecommendationChangeNotification } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
@@ -35,12 +34,10 @@ export class ExtensionIgnoredRecommendationsService extends Disposable implement
constructor(
@IWorkpsaceExtensionsConfigService private readonly workpsaceExtensionsConfigService: IWorkpsaceExtensionsConfigService,
@IStorageService private readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoredRecommendationsStorageKey, version: 1 });
this._globalIgnoredRecommendations = this.getCachedIgnoredRecommendations();
this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
this._register(this.storageService.onDidChangeValue(e => this.onDidStorageChange(e)));
this.initIgnoredWorkspaceRecommendations();
}
@@ -72,7 +69,7 @@ export class ExtensionIgnoredRecommendationsService extends Disposable implement
return ignoredRecommendations.map(e => e.toLowerCase());
}
private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void {
private onDidStorageChange(e: IStorageValueChangeEvent): void {
if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL
&& this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) {
this._ignoredRecommendationsValue = undefined;
@@ -106,7 +103,7 @@ export class ExtensionIgnoredRecommendationsService extends Disposable implement
}
private setStoredIgnoredRecommendationsValue(value: string): void {
this.storageService.store(ignoredRecommendationsStorageKey, value, StorageScope.GLOBAL);
this.storageService.store(ignoredRecommendationsStorageKey, value, StorageScope.GLOBAL, StorageTarget.USER);
}
}

View File

@@ -7,11 +7,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IStringDictionary } from 'vs/base/common/collections';
import { Event } from 'vs/base/common/event';
export interface IExtensionsConfigContent {
recommendations: string[];
unwantedRecommendations: string[];
}
export type DynamicRecommendation = 'dynamic';
export type ConfigRecommendation = 'config';
export type ExecutableRecommendation = 'executable';

View File

@@ -3,20 +3,28 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce, distinct, flatten } from 'vs/base/common/arrays';
import { distinct, flatten } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { parse } from 'vs/base/common/json';
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { FileKind, IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { localize } from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { IJSONEditingService, IJSONValue } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ResourceMap } from 'vs/base/common/map';
export const EXTENSIONS_CONFIG = '.vscode/extensions.json';
export interface IExtensionsConfigContent {
recommendations: string[];
unwantedRecommendations: string[];
recommendations?: string[];
unwantedRecommendations?: string[];
}
export const IWorkpsaceExtensionsConfigService = createDecorator<IWorkpsaceExtensionsConfigService>('IWorkpsaceExtensionsConfigService');
@@ -26,8 +34,11 @@ export interface IWorkpsaceExtensionsConfigService {
onDidChangeExtensionsConfigs: Event<void>;
getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>;
getRecommendations(): Promise<string[]>;
getUnwantedRecommendations(): Promise<string[]>;
toggleRecommendation(extensionId: string): Promise<void>;
toggleUnwantedRecommendation(extensionId: string): Promise<void>;
}
export class WorkspaceExtensionsConfigService extends Disposable implements IWorkpsaceExtensionsConfigService {
@@ -40,53 +51,217 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IFileService private readonly fileService: IFileService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
) {
super();
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire()));
this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire()));
this._register(fileService.onDidFilesChange(e => {
const workspace = workspaceContextService.getWorkspace();
if ((workspace.configuration && e.affects(workspace.configuration))
|| workspace.folders.some(folder => e.affects(folder.toResource(EXTENSIONS_CONFIG)))
) {
this._onDidChangeExtensionsConfigs.fire();
}
}));
}
async getExtensionsConfigs(): Promise<IExtensionsConfigContent[]> {
const workspace = this.workspaceContextService.getWorkspace();
const result = await Promise.all([
this.resolveWorkspaceExtensionConfig(workspace),
...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))
]);
return coalesce(result);
const result: IExtensionsConfigContent[] = [];
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
if (workspaceExtensionsConfigContent) {
result.push(workspaceExtensionsConfigContent);
}
result.push(...await Promise.all(workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))));
return result;
}
async getRecommendations(): Promise<string[]> {
const configs = await this.getExtensionsConfigs();
return distinct(flatten(configs.map(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : [])));
}
async getUnwantedRecommendations(): Promise<string[]> {
const configs = await this.getExtensionsConfigs();
return distinct(flatten(configs.map(c => c.unwantedRecommendations.map(c => c.toLowerCase()))));
return distinct(flatten(configs.map(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : [])));
}
private async resolveWorkspaceExtensionConfig(workspace: IWorkspace): Promise<IExtensionsConfigContent | null> {
try {
if (workspace.configuration) {
const content = await this.fileService.readFile(workspace.configuration);
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
return this.parseExtensionConfig(extensionsConfigContent);
async toggleRecommendation(extensionId: string): Promise<void> {
const workspace = this.workspaceContextService.getWorkspace();
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();
await Promise.all(workspace.folders.map(async workspaceFolder => {
const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);
workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);
}));
const isWorkspaceRecommended = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.recommendations?.some(r => r === extensionId);
const recommendedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.recommendations?.some(r => r === extensionId));
const isRecommended = isWorkspaceRecommended || recommendedWorksapceFolders.length > 0;
const workspaceOrFolders = isRecommended
? await this.pickWorkspaceOrFolders(recommendedWorksapceFolders, isWorkspaceRecommended ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))
: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));
for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {
if (IWorkspace.isIWorkspace(workspaceOrWorkspaceFolder)) {
await this.addOrRemoveWorkspaceRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isRecommended);
} else {
await this.addOrRemoveWorkspaceFolderRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isRecommended);
}
} catch (e) { /* Ignore */ }
return null;
}
}
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent | null> {
async toggleUnwantedRecommendation(extensionId: string): Promise<void> {
const workspace = this.workspaceContextService.getWorkspace();
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();
await Promise.all(workspace.folders.map(async workspaceFolder => {
const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);
workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);
}));
const isWorkspaceUnwanted = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.unwantedRecommendations?.some(r => r === extensionId);
const unWantedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.unwantedRecommendations?.some(r => r === extensionId));
const isUnwanted = isWorkspaceUnwanted || unWantedWorksapceFolders.length > 0;
const workspaceOrFolders = isUnwanted
? await this.pickWorkspaceOrFolders(unWantedWorksapceFolders, isWorkspaceUnwanted ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))
: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));
for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {
if (IWorkspace.isIWorkspace(workspaceOrWorkspaceFolder)) {
await this.addOrRemoveWorkspaceUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isUnwanted);
} else {
await this.addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isUnwanted);
}
}
}
private async addOrRemoveWorkspaceFolderRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (add) {
values.push({ path: ['recommendations'], value: [...extensionsConfigContent.recommendations || [], extensionId] });
if (extensionsConfigContent.unwantedRecommendations && extensionsConfigContent.unwantedRecommendations.some(e => e === extensionId)) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.recommendations) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
if (values.length) {
return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);
}
}
private async addOrRemoveWorkspaceRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (extensionsConfigContent) {
if (add) {
values.push({ path: ['recommendations'], value: [...extensionsConfigContent.recommendations || [], extensionId] });
if (extensionsConfigContent.unwantedRecommendations && extensionsConfigContent.unwantedRecommendations.some(e => e === extensionId)) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.recommendations) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
} else if (add) {
values.push({ path: ['extensions'], value: { recommendations: [extensionId] } });
}
if (values.length) {
return this.jsonEditingService.write(workspace.configuration!, values, true);
}
}
private async addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (add) {
values.push({ path: ['unwantedRecommendations'], value: [...extensionsConfigContent.unwantedRecommendations || [], extensionId] });
if (extensionsConfigContent.recommendations && extensionsConfigContent.recommendations.some(e => e === extensionId)) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.unwantedRecommendations) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
if (values.length) {
return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);
}
}
private async addOrRemoveWorkspaceUnwantedRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (extensionsConfigContent) {
if (add) {
values.push({ path: ['unwantedRecommendations'], value: [...extensionsConfigContent.unwantedRecommendations || [], extensionId] });
if (extensionsConfigContent.recommendations && extensionsConfigContent.recommendations.some(e => e === extensionId)) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.unwantedRecommendations) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
} else if (add) {
values.push({ path: ['extensions'], value: { unwantedRecommendations: [extensionId] } });
}
if (values.length) {
return this.jsonEditingService.write(workspace.configuration!, values, true);
}
}
private async pickWorkspaceOrFolders(workspaceFolders: IWorkspaceFolder[], workspace: IWorkspace | undefined, placeHolder: string): Promise<(IWorkspace | IWorkspaceFolder)[]> {
const workspaceOrFolders = workspace ? [...workspaceFolders, workspace] : [...workspaceFolders];
if (workspaceOrFolders.length === 1) {
return workspaceOrFolders;
}
const folderPicks: (IQuickPickItem & { workspaceOrFolder: IWorkspace | IWorkspaceFolder } | IQuickPickSeparator)[] = workspaceFolders.map(workspaceFolder => {
return {
label: workspaceFolder.name,
description: localize('workspace folder', "Workspace Folder"),
workspaceOrFolder: workspaceFolder,
iconClasses: getIconClasses(this.modelService, this.modeService, workspaceFolder.uri, FileKind.ROOT_FOLDER)
};
});
if (workspace) {
folderPicks.push({ type: 'separator' });
folderPicks.push({
label: localize('workspace', "Workspace"),
workspaceOrFolder: workspace,
});
}
const result = await this.quickInputService.pick(folderPicks, { placeHolder, canPickMany: true }) || [];
return result.map(r => r.workspaceOrFolder!);
}
private async resolveWorkspaceExtensionConfig(workspaceConfigurationResource: URI): Promise<IExtensionsConfigContent | undefined> {
try {
const content = await this.fileService.readFile(workspaceConfigurationResource);
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
return extensionsConfigContent ? this.parseExtensionConfig(extensionsConfigContent) : undefined;
} catch (e) { /* Ignore */ }
return undefined;
}
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent> {
try {
const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG));
const extensionsConfigContent = <IExtensionsConfigContent>parse(content.value.toString());
return this.parseExtensionConfig(extensionsConfigContent);
} catch (e) { /* ignore */ }
return null;
return {};
}
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent | undefined): IExtensionsConfigContent | null {
if (extensionsConfigContent) {
return {
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
};
}
return null;
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent {
return {
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
};
}
}

View File

@@ -54,7 +54,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
new ExtensionRunningLocationClassifier(
productService,
configurationService,
(extensionKinds, isInstalledLocally, isInstalledRemotely) => this._pickRunningLocation(extensionKinds, isInstalledLocally, isInstalledRemotely)
(extensionKinds, isInstalledLocally, isInstalledRemotely) => ExtensionService.pickRunningLocation(extensionKinds, isInstalledLocally, isInstalledRemotely)
),
instantiationService,
notificationService,
@@ -137,11 +137,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten
};
}
private _pickRunningLocation(extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean): ExtensionRunningLocation {
public static pickRunningLocation(extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean): ExtensionRunningLocation {
let canRunRemotely = false;
for (const extensionKind of extensionKinds) {
if (extensionKind === 'ui' && isInstalledRemotely) {
// ui extensions run remotely if possible
return ExtensionRunningLocation.Remote;
// ui extensions run remotely if possible (but only as a last resort)
canRunRemotely = true;
}
if (extensionKind === 'workspace' && isInstalledRemotely) {
// workspace extensions run remotely if possible
@@ -152,7 +153,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
return ExtensionRunningLocation.LocalWebWorker;
}
}
return ExtensionRunningLocation.None;
return (canRunRemotely ? ExtensionRunningLocation.Remote : ExtensionRunningLocation.None);
}
protected _createExtensionHosts(_isInitialStart: boolean): IExtensionHost[] {

View File

@@ -13,7 +13,7 @@ import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbe
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IURLHandler, IURLService, IOpenURLOptions } from 'vs/platform/url/common/url';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@@ -26,24 +26,25 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
const FIVE_MINUTES = 5 * 60 * 1000;
const THIRTY_SECONDS = 30 * 1000;
const URL_TO_HANDLE = 'extensionUrlHandler.urlToHandle';
const CONFIRMED_EXTENSIONS_CONFIGURATION_KEY = 'extensions.confirmedUriHandlerExtensionIds';
const CONFIRMED_EXTENSIONS_STORAGE_KEY = 'extensionUrlHandler.confirmedExtensions';
const USER_TRUSTED_EXTENSIONS_CONFIGURATION_KEY = 'extensions.confirmedUriHandlerExtensionIds';
const USER_TRUSTED_EXTENSIONS_STORAGE_KEY = 'extensionUrlHandler.confirmedExtensions';
function isExtensionId(value: string): boolean {
return /^[a-z0-9][a-z0-9\-]*\.[a-z0-9][a-z0-9\-]*$/i.test(value);
}
class ConfirmedExtensionIdStorage {
class UserTrustedExtensionIdStorage {
get extensions(): string[] {
const confirmedExtensionIdsJson = this.storageService.get(CONFIRMED_EXTENSIONS_STORAGE_KEY, StorageScope.GLOBAL, '[]');
const userTrustedExtensionIdsJson = this.storageService.get(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, StorageScope.GLOBAL, '[]');
try {
return JSON.parse(confirmedExtensionIdsJson);
return JSON.parse(userTrustedExtensionIdsJson);
} catch {
return [];
}
@@ -60,7 +61,7 @@ class ConfirmedExtensionIdStorage {
}
set(ids: string[]): void {
this.storageService.store(CONFIRMED_EXTENSIONS_STORAGE_KEY, JSON.stringify(ids), StorageScope.GLOBAL);
this.storageService.store(USER_TRUSTED_EXTENSIONS_STORAGE_KEY, JSON.stringify(ids), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}
@@ -87,7 +88,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
private extensionHandlers = new Map<string, IURLHandler>();
private uriBuffer = new Map<string, { timestamp: number, uri: URI }[]>();
private storage: ConfirmedExtensionIdStorage;
private userTrustedExtensionsStorage: UserTrustedExtensionIdStorage;
private disposable: IDisposable;
constructor(
@@ -101,9 +102,10 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IStorageService private readonly storageService: IStorageService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IProgressService private readonly progressService: IProgressService
@IProgressService private readonly progressService: IProgressService,
@IExtensionUrlTrustService private readonly extensionUrlTrustService: IExtensionUrlTrustService
) {
this.storage = new ConfirmedExtensionIdStorage(storageService);
this.userTrustedExtensionsStorage = new UserTrustedExtensionIdStorage(storageService);
const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS);
const urlToHandleValue = this.storageService.get(URL_TO_HANDLE, StorageScope.WORKSPACE);
@@ -118,7 +120,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
);
const cache = ExtensionUrlBootstrapHandler.cache;
setTimeout(() => cache.forEach(uri => this.handleURL(uri)));
setTimeout(() => cache.forEach(([uri, option]) => this.handleURL(uri, option)));
}
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
@@ -135,14 +137,11 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
return true;
}
let showConfirm: boolean;
if (options && options.trusted) {
showConfirm = false;
} else {
showConfirm = !this.isConfirmed(ExtensionIdentifier.toKey(extensionId));
}
const trusted = options?.trusted
|| (options?.originalUrl ? await this.extensionUrlTrustService.isExtensionUrlTrusted(extensionId, options.originalUrl) : false)
|| this.didUserTrustExtension(ExtensionIdentifier.toKey(extensionId));
if (showConfirm) {
if (!trusted) {
let uriString = uri.toString(false);
if (uriString.length > 40) {
@@ -164,7 +163,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
}
if (result.checkboxChecked) {
this.storage.add(ExtensionIdentifier.toKey(extensionId));
this.userTrustedExtensionsStorage.add(ExtensionIdentifier.toKey(extensionId));
}
}
@@ -293,7 +292,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
}
private async reloadAndHandle(url: URI): Promise<void> {
this.storageService.store(URL_TO_HANDLE, JSON.stringify(url.toJSON()), StorageScope.WORKSPACE);
this.storageService.store(URL_TO_HANDLE, JSON.stringify(url.toJSON()), StorageScope.WORKSPACE, StorageTarget.MACHINE);
await this.hostService.reload();
}
@@ -313,22 +312,22 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
this.uriBuffer = uriBuffer;
}
private isConfirmed(id: string): boolean {
if (this.storage.has(id)) {
private didUserTrustExtension(id: string): boolean {
if (this.userTrustedExtensionsStorage.has(id)) {
return true;
}
return this.getConfirmedExtensionIdsFromConfiguration().indexOf(id) > -1;
return this.getConfirmedTrustedExtensionIdsFromConfiguration().indexOf(id) > -1;
}
private getConfirmedExtensionIdsFromConfiguration(): Array<string> {
const confirmedExtensionIds = this.configurationService.getValue<Array<string>>(CONFIRMED_EXTENSIONS_CONFIGURATION_KEY);
private getConfirmedTrustedExtensionIdsFromConfiguration(): Array<string> {
const trustedExtensionIds = this.configurationService.getValue<Array<string>>(USER_TRUSTED_EXTENSIONS_CONFIGURATION_KEY);
if (!Array.isArray(confirmedExtensionIds)) {
if (!Array.isArray(trustedExtensionIds)) {
return [];
}
return confirmedExtensionIds;
return trustedExtensionIds;
}
dispose(): void {
@@ -346,10 +345,10 @@ registerSingleton(IExtensionUrlHandler, ExtensionUrlHandler);
*/
class ExtensionUrlBootstrapHandler implements IWorkbenchContribution, IURLHandler {
private static _cache: URI[] = [];
private static _cache: [URI, IOpenURLOptions | undefined][] = [];
private static disposable: IDisposable;
static get cache(): URI[] {
static get cache(): [URI, IOpenURLOptions | undefined][] {
ExtensionUrlBootstrapHandler.disposable.dispose();
const result = ExtensionUrlBootstrapHandler._cache;
@@ -361,12 +360,12 @@ class ExtensionUrlBootstrapHandler implements IWorkbenchContribution, IURLHandle
ExtensionUrlBootstrapHandler.disposable = urlService.registerHandler(this);
}
async handleURL(uri: URI): Promise<boolean> {
async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
if (!isExtensionId(uri.authority)) {
return false;
}
ExtensionUrlBootstrapHandler._cache.push(uri);
ExtensionUrlBootstrapHandler._cache.push([uri, options]);
return true;
}
}
@@ -391,7 +390,7 @@ class ManageAuthorizedExtensionURIsAction extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const storageService = accessor.get(IStorageService);
const quickInputService = accessor.get(IQuickInputService);
const storage = new ConfirmedExtensionIdStorage(storageService);
const storage = new UserTrustedExtensionIdStorage(storageService);
const items = storage.extensions.map(label => ({ label, picked: true } as IQuickPickItem));
if (items.length === 0) {

View File

@@ -60,7 +60,6 @@ export class ExtensionHostMain {
const instaService: IInstantiationService = new InstantiationService(services, true);
// todo@joh
// ugly self - inject
const terminalService = instaService.invokeFunction(accessor => accessor.get(IExtHostTerminalService));
this._disposables.add(terminalService);
@@ -71,7 +70,6 @@ export class ExtensionHostMain {
logService.info('extension host started');
logService.trace('initData', initData);
// todo@joh
// ugly self - inject
// must call initialize *after* creating the extension service
// because `initialize` itself creates instances that depend on it

View File

@@ -273,18 +273,14 @@ export class CachedExtensionScanner {
finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions);
}
const userExtensions = (
!environmentService.extensionsPath
? Promise.resolve([])
: this._scanExtensionsWithCache(
hostService,
notificationService,
environmentService,
USER_MANIFEST_CACHE_FILE,
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
log
)
);
const userExtensions = (this._scanExtensionsWithCache(
hostService,
notificationService,
environmentService,
USER_MANIFEST_CACHE_FILE,
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
log
));
// Always load developed extensions while extensions development
let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]);

View File

@@ -468,9 +468,12 @@ let _caCertificates: ReturnType<typeof readCaCertificates> | Promise<undefined>;
async function getCaCertificates(extHostLogService: ILogService) {
if (!_caCertificates) {
_caCertificates = readCaCertificates()
.then(res => res && res.certs.length ? res : undefined)
.then(res => {
extHostLogService.debug('ProxyResolver#getCaCertificates count', res && res.certs.length);
return res && res.certs.length ? res : undefined;
})
.catch(err => {
extHostLogService.error('ProxyResolver#getCertificates', toErrorMessage(err));
extHostLogService.error('ProxyResolver#getCaCertificates error', toErrorMessage(err));
return undefined;
});
}

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ExtensionService as BrowserExtensionService } from 'vs/workbench/services/extensions/browser/extensionService';
import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/abstractExtensionService';
suite('BrowserExtensionService', () => {
test('pickRunningLocation', () => {
assert.deepEqual(BrowserExtensionService.pickRunningLocation([], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation([], false, true), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation([], true, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation([], true, true), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui'], true, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace'], true, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web'], false, true), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], true, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], true, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], true, true), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], true, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], false, false), ExtensionRunningLocation.None);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], false, true), ExtensionRunningLocation.Remote);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, false), ExtensionRunningLocation.LocalWebWorker);
assert.deepEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, true), ExtensionRunningLocation.Remote);
});
});

View File

@@ -8,18 +8,28 @@
let MonacoEnvironment = (<any>self).MonacoEnvironment;
let monacoBaseUrl = MonacoEnvironment && MonacoEnvironment.baseUrl ? MonacoEnvironment.baseUrl : '../../../../../';
const trustedTypesPolicy = self.trustedTypes?.createPolicy('amdLoader', { createScriptURL: value => value });
if (typeof (<any>self).define !== 'function' || !(<any>self).define.amd) {
importScripts(monacoBaseUrl + 'vs/loader.js');
let loaderSrc: string | TrustedScriptURL = monacoBaseUrl + 'vs/loader.js';
if (trustedTypesPolicy) {
loaderSrc = trustedTypesPolicy.createScriptURL(loaderSrc);
}
importScripts(loaderSrc as string);
}
require.config({
baseUrl: monacoBaseUrl,
catchError: true,
<<<<<<< HEAD
createTrustedScriptURL: (value: string) => value,
paths: {
'@coder/node-browser': `../node_modules/@coder/node-browser/out/client/client.js`,
'@coder/requirefs': `../node_modules/@coder/requirefs/out/requirefs.js`,
}
=======
trustedTypesPolicy
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
});
require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err));

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self' data:; script-src 'unsafe-eval' 'sha256-O98pkmgtvUCQGVoddaGy891K52PVRnySDRxRszVLPNQ=' http: https:; connect-src http: https:" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self' data:; script-src 'unsafe-eval' 'sha256-DhNBVT9y4y9LG937ZrEbN5CwALd+WSpQnG3z5u1MOFk=' http: https:; connect-src http: https:" />
</head>
<body>
<script>
@@ -25,14 +25,9 @@
worker.onmessage = (event) => {
const { data } = event;
if (!(data instanceof MessagePort)) {
console.warn('Unknown data received', event);
sendError({ name: 'Error', message: 'Unknown data received', stack: [] });
return;
}
window.parent.postMessage({
vscodeWebWorkerExtHostId,
data: data
data
}, '*', [data]);
};

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self' data:; script-src 'unsafe-eval' 'sha256-O98pkmgtvUCQGVoddaGy891K52PVRnySDRxRszVLPNQ=' https:; connect-src https:" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self' data:; script-src 'unsafe-eval' 'sha256-DhNBVT9y4y9LG937ZrEbN5CwALd+WSpQnG3z5u1MOFk=' https:; connect-src https:" />
</head>
<body>
<script>
@@ -25,14 +25,9 @@
worker.onmessage = (event) => {
const { data } = event;
if (!(data instanceof MessagePort)) {
console.warn('Unknown data received', event);
sendError({ name: 'Error', message: 'Unknown data received', stack: [] });
return;
}
window.parent.postMessage({
vscodeWebWorkerExtHostId,
data: data
data
}, '*', [data]);
};

View File

@@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
type GettingStartedItem = {
id: string
title: string,
description: string,
button: { title: string, command: string },
doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never, }
when?: string,
media: { type: 'image', path: string, altText: string },
};
type GettingStartedCategory = {
id: string
title: string,
description: string,
codicon: string,
when?: string,
content:
| { type: 'items', items: GettingStartedItem[] }
| { type: 'command', command: string }
};
type GettingStartedContent = GettingStartedCategory[];
export const content: GettingStartedContent = [
{
id: 'Beginner',
title: localize('gettingStarted.beginner.title', "Get Started"),
codicon: 'lightbulb',
description: localize('gettingStarted.beginner.description', "Get to know your new editor"),
content: {
type: 'items',
items: [
{
id: 'pickColorTheme',
description: localize('pickColorTask.description', "Modify the colors in the user interface to suit your preferences and work environment."),
title: localize('pickColorTask.title', "Color Theme"),
button: { title: localize('pickColorTask.button', "Find a Theme"), command: 'workbench.action.selectTheme' },
doneOn: { eventFired: 'themeSelected' },
media: { type: 'image', altText: 'ColorTheme', path: 'ColorTheme.jpg', }
},
{
id: 'findKeybindingsExtensions',
description: localize('findKeybindingsTask.description', "Find keyboard shortcuts for Vim, Sublime, Atom and others."),
title: localize('findKeybindingsTask.title', "Configure Keybindings"),
button: {
title: localize('findKeybindingsTask.button', "Search for Keymaps"),
command: 'workbench.extensions.action.showRecommendedKeymapExtensions'
},
doneOn: { commandExecuted: 'workbench.extensions.action.showRecommendedKeymapExtensions' },
media: { type: 'image', altText: 'Extensions', path: 'Extensions.jpg', }
},
{
id: 'findLanguageExtensions',
description: localize('findLanguageExtsTask.description', "Get support for your languages like JavaScript, Python, Java, Azure, Docker, and more."),
title: localize('findLanguageExtsTask.title', "Languages & Tools"),
button: {
title: localize('findLanguageExtsTask.button', "Install Language Support"),
command: 'workbench.extensions.action.showLanguageExtensions',
},
doneOn: { commandExecuted: 'workbench.extensions.action.showLanguageExtensions' },
media: { type: 'image', altText: 'Languages', path: 'Languages.jpg', }
},
{
id: 'pickAFolderTask-Mac',
description: localize('gettingStartedOpenFolder.description', "Open a project folder to get started!"),
title: localize('gettingStartedOpenFolder.title', "Open Folder"),
when: 'isMac',
button: {
title: localize('gettingStartedOpenFolder.button', "Pick a Folder"),
command: 'workbench.action.files.openFileFolder'
},
doneOn: { commandExecuted: 'workbench.action.files.openFileFolder' },
media: { type: 'image', altText: 'OpenFolder', path: 'OpenFolder.jpg' }
},
{
id: 'pickAFolderTask-Other',
description: localize('gettingStartedOpenFolder.description', "Open a project folder to get started!"),
title: localize('gettingStartedOpenFolder.title', "Open Folder"),
when: '!isMac',
button: {
title: localize('gettingStartedOpenFolder.button', "Pick a Folder"),
command: 'workbench.action.files.openFolder'
},
doneOn: { commandExecuted: 'workbench.action.files.openFolder' },
media: { type: 'image', altText: 'OpenFolder', path: 'OpenFolder.jpg' }
}
]
}
},
{
id: 'Intermediate',
title: localize('gettingStarted.intermediate.title', "Essentials"),
codicon: 'heart',
description: localize('gettingStarted.intermediate.description', "Must know features you'll love"),
content: {
type: 'items',
items: [
{
id: 'commandPaletteTask',
description: localize('commandPaletteTask.description', "The easiest way to find everything VS Code can do. If you\'re ever looking for a feature, check here first!"),
title: localize('commandPaletteTask.title', "Command Palette"),
button: {
title: localize('commandPaletteTask.button', "View All Commands"),
command: 'workbench.action.showCommands'
},
doneOn: { commandExecuted: 'workbench.action.showCommands' },
media: { type: 'image', altText: 'gif of a custom tree hover', path: 'https://code.visualstudio.com/assets/updates/1_51/custom-tree-hover.gif' },
}
]
}
},
{
id: 'Advanced',
title: localize('gettingStarted.advanced.title', "Tips & Tricks"),
codicon: 'tools',
description: localize('gettingStarted.advanced.description', "Favorites from VS Code experts"),
content: {
type: 'items',
items: []
}
},
{
id: 'OpenFolder-Mac',
title: localize('gettingStarted.openFolder.title', "Open Folder"),
codicon: 'folder-opened',
when: 'isMac',
description: localize('gettingStarted.openFolder.description', "Open a project and start working"),
content: {
type: 'command',
command: 'workbench.action.files.openFileFolder'
}
},
{
id: 'OpenFolder-Other',
title: localize('gettingStarted.openFolder.title', "Open Folder"),
codicon: 'folder-opened',
description: localize('gettingStarted.openFolder.description', "Open a project and start working"),
when: '!isMac',
content: {
type: 'command',
command: 'workbench.action.files.openFolder'
}
},
{
id: 'InteractivePlayground',
title: localize('gettingStarted.playground.title', "Interactive Playground"),
codicon: 'library',
description: localize('gettingStarted.interactivePlayground.description', "Learn essential editor features"),
content: {
type: 'command',
command: 'workbench.action.showInteractivePlayground'
}
}
];

View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { Registry } from 'vs/platform/registry/common/platform';
import { content } from 'vs/workbench/services/gettingStarted/common/gettingStartedContent';
export const enum GettingStartedCategory {
Beginner = 'Beginner',
Intermediate = 'Intermediate',
Advanced = 'Advanced'
}
export interface IGettingStartedTask {
id: string,
title: string,
description: string,
category: GettingStartedCategory | string,
when: ContextKeyExpression,
order: number,
button: { title: string, command: string },
doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never, }
media: { type: 'image', path: URI, altText: string },
}
export interface IGettingStartedCategoryDescriptor {
id: GettingStartedCategory | string
title: string
description: string
codicon: string
when: ContextKeyExpression
content:
| { type: 'items' }
| { type: 'command', command: string }
}
export interface IGettingStartedCategory {
id: GettingStartedCategory | string
title: string
description: string
codicon: string
when: ContextKeyExpression
content:
| { type: 'items', items: IGettingStartedTask[] }
| { type: 'command', command: string }
}
export interface IGettingStartedRegistry {
onDidAddCategory: Event<IGettingStartedCategory>
onDidAddTask: Event<IGettingStartedTask>
registerTask(task: IGettingStartedTask): IGettingStartedTask;
getTask(id: string): IGettingStartedTask
registerCategory(categoryDescriptor: IGettingStartedCategoryDescriptor): void
getCategory(id: GettingStartedCategory | string): Readonly<IGettingStartedCategory> | undefined
getCategories(): readonly Readonly<IGettingStartedCategory>[]
}
export class GettingStartedRegistryImpl implements IGettingStartedRegistry {
private readonly _onDidAddTask = new Emitter<IGettingStartedTask>();
onDidAddTask: Event<IGettingStartedTask> = this._onDidAddTask.event;
private readonly _onDidAddCategory = new Emitter<IGettingStartedCategory>();
onDidAddCategory: Event<IGettingStartedCategory> = this._onDidAddCategory.event;
private readonly gettingStartedContributions = new Map<string, IGettingStartedCategory>();
private readonly tasks = new Map<string, IGettingStartedTask>();
public registerTask(task: IGettingStartedTask): IGettingStartedTask {
const category = this.gettingStartedContributions.get(task.category);
if (!category) { throw Error('Registering getting started task to category that does not exist (' + task.category + ')'); }
if (category.content.type !== 'items') { throw Error('Registering getting started task to category that is not of `items` type (' + task.category + ')'); }
if (this.tasks.has(task.id)) { throw Error('Attempting to register task with id ' + task.id + ' twice. Second is dropped.'); }
this.tasks.set(task.id, task);
category.content.items.push(task);
this._onDidAddTask.fire(task);
return task;
}
public registerCategory(categoryDescriptor: IGettingStartedCategoryDescriptor): void {
const oldCategory = this.gettingStartedContributions.get(categoryDescriptor.id);
if (oldCategory) {
console.error(`Skipping attempt to overwrite getting started category. (${categoryDescriptor})`);
return;
}
const category: IGettingStartedCategory = {
...categoryDescriptor,
content: categoryDescriptor.content.type === 'items'
? { type: 'items', items: [] }
: categoryDescriptor.content
};
this.gettingStartedContributions.set(categoryDescriptor.id, category);
this._onDidAddCategory.fire(category);
}
public getCategory(id: GettingStartedCategory | string): Readonly<IGettingStartedCategory> | undefined {
return this.gettingStartedContributions.get(id);
}
public getTask(id: string): IGettingStartedTask {
const task = this.tasks.get(id);
if (!task) { throw Error('Attempting to access task which does not exist in registry ' + id); }
return task;
}
public getCategories(): readonly Readonly<IGettingStartedCategory>[] {
return [...this.gettingStartedContributions.values()];
}
}
export const GettingStartedRegistryID = 'GettingStartedRegistry';
const registryImpl = new GettingStartedRegistryImpl();
content.forEach(category => {
registryImpl.registerCategory({
...category,
when: ContextKeyExpr.deserialize(category.when) ?? ContextKeyExpr.true()
});
if (category.content.type === 'items') {
category.content.items.forEach((item, index) => {
registryImpl.registerTask({
...item,
category: category.id,
order: index,
when: ContextKeyExpr.deserialize(item.when) ?? ContextKeyExpr.true(),
media: {
type: item.media.type,
altText: item.media.altText,
path: item.media.path.startsWith('https://')
? URI.parse(item.media.path, true)
: FileAccess.asFileUri('vs/workbench/services/gettingStarted/common/media/' + item.media.path, require)
}
});
});
}
});
Registry.add(GettingStartedRegistryID, registryImpl);
export const GettingStartedRegistry: IGettingStartedRegistry = Registry.as(GettingStartedRegistryID);

View File

@@ -0,0 +1,195 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IGettingStartedTask, GettingStartedRegistry, IGettingStartedCategory, } from 'vs/workbench/services/gettingStarted/common/gettingStartedRegistry';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { Memento } from 'vs/workbench/common/memento';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
export const IGettingStartedService = createDecorator<IGettingStartedService>('gettingStartedService');
type TaskProgress = { done: boolean; };
export interface IGettingStartedTaskWithProgress extends IGettingStartedTask, TaskProgress { }
export interface IGettingStartedCategoryWithProgress extends Omit<IGettingStartedCategory, 'content'> {
content:
| {
type: 'items',
items: IGettingStartedTaskWithProgress[],
done: boolean;
stepsComplete: number
stepsTotal: number
}
| { type: 'command', command: string }
}
export interface IGettingStartedService {
_serviceBrand: undefined,
readonly onDidAddTask: Event<IGettingStartedTaskWithProgress>
readonly onDidAddCategory: Event<IGettingStartedCategoryWithProgress>
readonly onDidProgressTask: Event<IGettingStartedTaskWithProgress>
getCategories(): IGettingStartedCategoryWithProgress[]
progressByEvent(eventName: string): void;
}
export class GettingStartedService implements IGettingStartedService {
declare readonly _serviceBrand: undefined;
private readonly _onDidAddTask = new Emitter<IGettingStartedTaskWithProgress>();
onDidAddTask: Event<IGettingStartedTaskWithProgress> = this._onDidAddTask.event;
private readonly _onDidAddCategory = new Emitter<IGettingStartedCategoryWithProgress>();
onDidAddCategory: Event<IGettingStartedCategoryWithProgress> = this._onDidAddCategory.event;
private readonly _onDidProgressTask = new Emitter<IGettingStartedTaskWithProgress>();
onDidProgressTask: Event<IGettingStartedTaskWithProgress> = this._onDidProgressTask.event;
private registry = GettingStartedRegistry;
private memento: Memento;
private taskProgress: Record<string, TaskProgress>;
private commandListeners = new Map<string, string[]>();
private eventListeners = new Map<string, string[]>();
constructor(
@IStorageService private readonly storageService: IStorageService,
@ICommandService private readonly commandService: ICommandService,
@IContextKeyService private readonly contextService: IContextKeyService,
) {
this.memento = new Memento('gettingStartedService', this.storageService);
this.taskProgress = this.memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER);
this.registry.getCategories().forEach(category => {
if (category.content.type === 'items') {
category.content.items.forEach(task => this.registerDoneListeners(task));
}
});
this.registry.onDidAddCategory(category => this._onDidAddCategory.fire(this.getCategoryProgress(category)));
this.registry.onDidAddTask(task => {
this.registerDoneListeners(task);
this._onDidAddTask.fire(this.getTaskProgress(task));
});
this.commandService.onDidExecuteCommand(command => this.progressByCommand(command.commandId));
}
private registerDoneListeners(task: IGettingStartedTask) {
if (task.doneOn.commandExecuted) {
const existing = this.commandListeners.get(task.doneOn.commandExecuted);
if (existing) { existing.push(task.id); }
else {
this.commandListeners.set(task.doneOn.commandExecuted, [task.id]);
}
}
if (task.doneOn.eventFired) {
const existing = this.eventListeners.get(task.doneOn.eventFired);
if (existing) { existing.push(task.id); }
else {
this.eventListeners.set(task.doneOn.eventFired, [task.id]);
}
}
}
getCategories(): IGettingStartedCategoryWithProgress[] {
const registeredCategories = this.registry.getCategories();
const categoriesWithCompletion = registeredCategories
.filter(category => this.contextService.contextMatchesRules(category.when))
.map(category => {
if (category.content.type === 'items') {
return {
...category,
content: {
type: 'items' as const,
items: category.content.items.filter(item => this.contextService.contextMatchesRules(item.when))
}
};
}
return category;
})
.filter(category => category.content.type !== 'items' || category.content.items.length)
.map(category => this.getCategoryProgress(category));
return categoriesWithCompletion;
}
private getCategoryProgress(category: IGettingStartedCategory): IGettingStartedCategoryWithProgress {
if (category.content.type === 'command') {
return { ...category, content: category.content };
}
const tasksWithProgress = category.content.items.map(task => this.getTaskProgress(task));
const tasksComplete = tasksWithProgress.filter(task => task.done);
return {
...category,
content: {
type: 'items',
items: tasksWithProgress,
stepsComplete: tasksComplete.length,
stepsTotal: tasksWithProgress.length,
done: tasksComplete.length === tasksWithProgress.length,
}
};
}
private getTaskProgress(task: IGettingStartedTask): IGettingStartedTaskWithProgress {
return {
...task,
...this.taskProgress[task.id]
};
}
private progressTask(id: string) {
const oldProgress = this.taskProgress[id];
if (!oldProgress || oldProgress.done !== true) {
this.taskProgress[id] = { done: true };
this.memento.saveMemento();
const task = this.registry.getTask(id);
this._onDidProgressTask.fire(this.getTaskProgress(task));
}
}
private progressByCommand(command: string) {
const listening = this.commandListeners.get(command) ?? [];
listening.forEach(id => this.progressTask(id));
}
progressByEvent(event: string): void {
const listening = this.eventListeners.get(event) ?? [];
listening.forEach(id => this.progressTask(id));
}
}
registerAction2(class extends Action2 {
constructor() {
super({
id: 'resetGettingStartedProgress',
category: 'Getting Started',
title: 'Reset Progress',
f1: true
});
}
run(accessor: ServicesAccessor) {
const memento = new Memento('gettingStartedService', accessor.get(IStorageService));
const record = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER);
for (const key in record) {
if (Object.prototype.hasOwnProperty.call(record, key)) {
delete record[key];
}
}
memento.saveMemento();
}
});
registerSingleton(IGettingStartedService, GettingStartedService);

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -13,7 +13,7 @@ import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG }
import { Selection } from 'vs/editor/common/core/selection';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -517,7 +517,7 @@ export class HistoryService extends Disposable implements IHistoryService {
private preferResourceEditorInput(input: IEditorInput): IEditorInput | IResourceEditorInput {
const resource = EditorResourceAccessor.getOriginalUri(input);
if (resource && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData || resource.scheme === this.pathService.defaultUriScheme)) {
if (resource?.scheme === Schemas.file || resource?.scheme === Schemas.vscodeRemote || resource?.scheme === Schemas.userData || resource?.scheme === this.pathService.defaultUriScheme) {
// for now, only prefer well known schemes that we control to prevent
// issues such as https://github.com/microsoft/vscode/issues/85204
return { resource };
@@ -962,7 +962,7 @@ export class HistoryService extends Disposable implements IHistoryService {
return undefined;
}));
this.storageService.store(HistoryService.HISTORY_STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE);
this.storageService.store(HistoryService.HISTORY_STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE, StorageTarget.MACHINE);
}
//#endregion

View File

@@ -244,7 +244,8 @@ export class BrowserHostService extends Disposable implements IHostService {
if (this.shouldReuse(options, true /* file */)) {
editorService.openEditor({
leftResource: editors[0].resource,
rightResource: editors[1].resource
rightResource: editors[1].resource,
options: { pinned: true }
});
}
@@ -349,7 +350,7 @@ export class BrowserHostService extends Disposable implements IHostService {
return true; // always handle --wait in same window
}
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const windowConfig = this.configurationService.getValue<IWindowSettings | undefined>('window');
const openInNewWindowConfig = isFile ? (windowConfig?.openFilesInNewWindow || 'off' /* default */) : (windowConfig?.openFoldersInNewWindow || 'default' /* default */);
let openInNewWindow = (options.preferNewWindow || !!options.forceNewWindow) && !options.forceReuseWindow;

View File

@@ -81,7 +81,7 @@ export interface IHostService {
/**
* Reload the currently active window.
*/
reload(): Promise<void>;
reload(options?: { disableExtensions?: boolean }): Promise<void>;
/**
* Attempt to close the active window.

View File

@@ -102,8 +102,8 @@ export class NativeHostService extends Disposable implements IHostService {
return this.nativeHostService.relaunch();
}
reload(): Promise<void> {
return this.nativeHostService.reload();
reload(options?: { disableExtensions?: boolean }): Promise<void> {
return this.nativeHostService.reload(options);
}
close(): Promise<void> {

View File

@@ -16,7 +16,6 @@ import { Widget } from 'vs/base/browser/ui/widget';
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { MarkdownString, MarkdownStringTextNewlineStyle } from 'vs/base/common/htmlContent';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
@@ -82,25 +81,29 @@ export class HoverWidget extends Widget {
const rowElement = $('div.hover-row.markdown-hover');
const contentsElement = $('div.hover-contents');
const markdown = typeof options.text === 'string' ? new MarkdownString().appendText(options.text, MarkdownStringTextNewlineStyle.Break) : options.text;
if (typeof options.text === 'string') {
contentsElement.textContent = options.text;
contentsElement.style.whiteSpace = 'pre-wrap';
} else {
const markdown = options.text;
const mdRenderer = this._instantiationService.createInstance(
MarkdownRenderer,
{ codeBlockFontFamily: this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily }
);
const mdRenderer = this._instantiationService.createInstance(
MarkdownRenderer,
{ codeBlockFontFamily: this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily }
);
const { element } = mdRenderer.render(markdown, {
actionHandler: {
callback: (content) => this._linkHandler(content),
disposeables: this._messageListeners
},
codeBlockRenderCallback: () => {
contentsElement.classList.add('code-hover-contents');
// This changes the dimensions of the hover so trigger a layout
this._onRequestLayout.fire();
}
});
contentsElement.appendChild(element);
const { element } = mdRenderer.render(markdown, {
actionHandler: {
callback: (content) => this._linkHandler(content),
disposeables: this._messageListeners
},
asyncRenderCallback: () => {
contentsElement.classList.add('code-hover-contents');
// This changes the dimensions of the hover so trigger a layout
this._onRequestLayout.fire();
}
});
contentsElement.appendChild(element);
}
rowElement.appendChild(contentsElement);
this._hover.contentsDomNode.appendChild(rowElement);
@@ -182,7 +185,6 @@ export class HoverWidget extends Widget {
} else {
const targetBottom = Math.max(...targetBounds.map(e => e.bottom));
if (targetBottom + this._hover.containerDomNode.clientHeight > window.innerHeight) {
console.log(targetBottom, this._hover.containerDomNode.clientHeight, window.innerHeight);
const targetTop = Math.min(...targetBounds.map(e => e.top));
this._anchor = AnchorPosition.ABOVE;
this._y = targetTop;

View File

@@ -12,7 +12,7 @@ import { ChecksumPair, IIntegrityService, IntegrityTestResult } from 'vs/workben
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { FileAccess } from 'vs/base/common/network';
@@ -51,7 +51,7 @@ class IntegrityStorage {
set(data: IStorageData | null): void {
this.value = data;
this.storageService.store(IntegrityStorage.KEY, JSON.stringify(this.value), StorageScope.GLOBAL);
this.storageService.store(IntegrityStorage.KEY, JSON.stringify(this.value), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IssueReporterData } from 'vs/platform/issue/common/issue';
export const IWorkbenchIssueService = createDecorator<IWorkbenchIssueService>('workbenchIssueService');
export interface IWorkbenchIssueService {
readonly _serviceBrand: undefined;
openReporter(dataOverrides?: Partial<IssueReporterData>): Promise<void>;
openProcessExplorer(): Promise<void>;
}

View File

@@ -28,7 +28,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IUserKeybindingItem, KeybindingIO, OutputBuilder } from 'vs/workbench/services/keybinding/common/keybindingIO';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { MenuRegistry } from 'vs/platform/actions/common/actions';
@@ -40,8 +40,8 @@ import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { parse } from 'vs/base/common/json';
import * as objects from 'vs/base/common/objects';
import { IKeymapService } from 'vs/workbench/services/keybinding/common/keymapInfo';
import { getDispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig';
import { IKeyboardLayoutService } from 'vs/platform/keyboardLayout/common/keyboardLayout';
import { getDispatchConfig } from 'vs/platform/keyboardLayout/common/dispatchConfig';
import { isArray } from 'vs/base/common/types';
import { INavigatorWithKeyboard, IKeyboard } from 'vs/workbench/services/keybinding/browser/navigatorKeyboard';
import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode';
@@ -49,6 +49,7 @@ import { flatten } from 'vs/base/common/arrays';
import { BrowserFeatures, KeyboardSupport } from 'vs/base/browser/canIUse';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { dirname } from 'vs/base/common/resources';
interface ContributedKeyBinding {
command: string;
@@ -194,7 +195,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
@IExtensionService extensionService: IExtensionService,
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@IKeymapService private readonly keymapService: IKeymapService
@IKeyboardLayoutService private readonly keyboardLayoutService: IKeyboardLayoutService
) {
super(contextKeyService, commandService, telemetryService, notificationService, logService);
@@ -209,13 +210,13 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
dispatchConfig = newDispatchConfig;
this._keyboardMapper = this.keymapService.getKeyboardMapper(dispatchConfig);
this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper(dispatchConfig);
this.updateResolver({ source: KeybindingSource.Default });
});
this._keyboardMapper = this.keymapService.getKeyboardMapper(dispatchConfig);
this.keymapService.onDidChangeKeyboardMapper(() => {
this._keyboardMapper = this.keymapService.getKeyboardMapper(dispatchConfig);
this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper(dispatchConfig);
this.keyboardLayoutService.onDidChangeKeyboardLayout(() => {
this._keyboardMapper = this.keyboardLayoutService.getKeyboardMapper(dispatchConfig);
this.updateResolver({ source: KeybindingSource.Default });
});
@@ -261,7 +262,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
this.isComposingGlobalContextKey.set(false);
}));
let data = this.keymapService.getCurrentKeyboardLayout();
let data = this.keyboardLayoutService.getCurrentKeyboardLayout();
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
@@ -325,16 +326,16 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
public _dumpDebugInfo(): string {
const layoutInfo = JSON.stringify(this.keymapService.getCurrentKeyboardLayout(), null, '\t');
const layoutInfo = JSON.stringify(this.keyboardLayoutService.getCurrentKeyboardLayout(), null, '\t');
const mapperInfo = this._keyboardMapper.dumpDebugInfo();
const rawMapping = JSON.stringify(this.keymapService.getRawKeyboardMapping(), null, '\t');
const rawMapping = JSON.stringify(this.keyboardLayoutService.getRawKeyboardMapping(), null, '\t');
return `Layout info:\n${layoutInfo}\n${mapperInfo}\n\nRaw mapping:\n${rawMapping}`;
}
public _dumpDebugInfoJSON(): string {
const info = {
layout: this.keymapService.getCurrentKeyboardLayout(),
rawMapping: this.keymapService.getRawKeyboardMapping()
layout: this.keyboardLayoutService.getCurrentKeyboardLayout(),
rawMapping: this.keyboardLayoutService.getRawKeyboardMapping()
};
return JSON.stringify(info, null, '\t');
}
@@ -371,7 +372,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
const keybinding = item.keybinding;
if (!keybinding) {
// This might be a removal keybinding item in user settings => accept it
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, item.extensionId);
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, item.extensionId, item.isBuiltinExtension);
} else {
if (this._assertBrowserConflicts(keybinding, item.command)) {
continue;
@@ -380,7 +381,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
const resolvedKeybindings = this.resolveKeybinding(keybinding);
for (let i = resolvedKeybindings.length - 1; i >= 0; i--) {
const resolvedKeybinding = resolvedKeybindings[i];
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, item.extensionId);
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, item.extensionId, item.isBuiltinExtension);
}
}
}
@@ -395,11 +396,11 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
const parts = item.parts;
if (parts.length === 0) {
// This might be a removal keybinding item in user settings => accept it
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, null);
result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, null, false);
} else {
const resolvedKeybindings = this._keyboardMapper.resolveUserBinding(parts);
for (const resolvedKeybinding of resolvedKeybindings) {
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, null);
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, null, false);
}
}
}
@@ -440,23 +441,26 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
partModifiersMask |= KeyMod.WinCtrl;
}
if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd && part.keyCode === KeyCode.KEY_W) {
// console.warn('Ctrl/Cmd+W keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
// re https://github.com/microsoft/vscode/issues/108788.
// since we introduced `window.confirmBeforeQuit`, we should probably not unbind cmd+w/t/n.
return true;
}
// if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd && part.keyCode === KeyCode.KEY_W) {
// // console.warn('Ctrl/Cmd+W keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd && part.keyCode === KeyCode.KEY_N) {
// console.warn('Ctrl/Cmd+N keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
// return true;
// }
return true;
}
// if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd && part.keyCode === KeyCode.KEY_N) {
// // console.warn('Ctrl/Cmd+N keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd && part.keyCode === KeyCode.KEY_T) {
// console.warn('Ctrl/Cmd+T keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
// return true;
// }
return true;
}
// if ((partModifiersMask & modifiersMask) === KeyMod.CtrlCmd && part.keyCode === KeyCode.KEY_T) {
// // console.warn('Ctrl/Cmd+T keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
// return true;
// }
if ((partModifiersMask & modifiersMask) === (KeyMod.CtrlCmd | KeyMod.Alt) && (part.keyCode === KeyCode.LeftArrow || part.keyCode === KeyCode.RightArrow)) {
// console.warn('Ctrl/Cmd+Arrow keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
@@ -479,7 +483,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
this.keymapService.validateCurrentKeyboardMapping(keyboardEvent);
this.keyboardLayoutService.validateCurrentKeyboardMapping(keyboardEvent);
return this._keyboardMapper.resolveKeyboardEvent(keyboardEvent);
}
@@ -550,7 +554,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
mac: mac ? { primary: KeybindingParser.parseKeybinding(mac, OS) } : null,
linux: linux ? { primary: KeybindingParser.parseKeybinding(linux, OS) } : null,
win: win ? { primary: KeybindingParser.parseKeybinding(win, OS) } : null,
extensionId: extensionId.value
extensionId: extensionId.value,
isBuiltinExtension: isBuiltin
};
if (!desc.primary && !desc.mac && !desc.linux && !desc.win) {
@@ -626,7 +631,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
// consult the KeyboardMapperFactory to check the given event for
// a printable value.
const mapping = this.keymapService.getRawKeyboardMapping();
const mapping = this.keyboardLayoutService.getRawKeyboardMapping();
if (!mapping) {
return false;
}
@@ -658,6 +663,7 @@ class UserKeybindings extends Disposable {
) {
super();
this._register(fileService.watch(dirname(keybindingsResource)));
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(changed => {
if (changed) {
this._onDidChange.fire();

View File

@@ -6,15 +6,15 @@
import * as nls from 'vs/nls';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { IKeymapService, IKeyboardLayoutInfo, IKeyboardMapping, IWindowsKeyboardMapping, KeymapInfo, IRawMixedKeyboardMapping, getKeyboardLayoutId, IKeymapInfo } from 'vs/workbench/services/keybinding/common/keymapInfo';
import { KeymapInfo, IRawMixedKeyboardMapping, IKeymapInfo } from 'vs/workbench/services/keybinding/common/keymapInfo';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { DispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig';
import { IKeyboardMapper, CachedKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { DispatchConfig } from 'vs/platform/keyboardLayout/common/dispatchConfig';
import { IKeyboardMapper, CachedKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { OS, OperatingSystem, isMacintosh, isWindows } from 'vs/base/common/platform';
import { WindowsKeyboardMapper } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
import { MacLinuxFallbackKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { IMacLinuxKeyboardMapping, MacLinuxKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { MacLinuxKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
@@ -29,6 +29,7 @@ import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/browser
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { getKeyboardLayoutId, IKeyboardLayoutInfo, IKeyboardLayoutService, IKeyboardMapping, IMacLinuxKeyboardMapping, IWindowsKeyboardMapping } from 'vs/platform/keyboardLayout/common/keyboardLayout';
export class BrowserKeyboardMapperFactoryBase {
// keyboard mapper
@@ -502,11 +503,11 @@ class UserKeyboardLayout extends Disposable {
}
class BrowserKeymapService extends Disposable implements IKeymapService {
export class BrowserKeyboardLayoutService extends Disposable implements IKeyboardLayoutService {
public _serviceBrand: undefined;
private readonly _onDidChangeKeyboardMapper = new Emitter<void>();
public readonly onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
private readonly _onDidChangeKeyboardLayout = new Emitter<void>();
public readonly onDidChangeKeyboardLayout: Event<void> = this._onDidChangeKeyboardLayout.event;
private _userKeyboardLayout: UserKeyboardLayout;
@@ -592,7 +593,7 @@ class BrowserKeymapService extends Disposable implements IKeymapService {
registerKeyboardListener() {
this.layoutChangeListener.value = this._factory.onDidChangeKeyboardMapper(() => {
this._onDidChangeKeyboardMapper.fire();
this._onDidChangeKeyboardLayout.fire();
});
}
@@ -617,7 +618,7 @@ class BrowserKeymapService extends Disposable implements IKeymapService {
}
}
registerSingleton(IKeymapService, BrowserKeymapService, true);
registerSingleton(IKeyboardLayoutService, BrowserKeyboardLayoutService, true);
// Configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration);

View File

@@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export const enum DispatchConfig {
Code,
KeyCode
}
export function getDispatchConfig(configurationService: IConfigurationService): DispatchConfig {
const keyboard = configurationService.getValue('keyboard');
const r = (keyboard ? (<any>keyboard).dispatch : null);
return (r === 'keyCode' ? DispatchConfig.KeyCode : DispatchConfig.Code);
}

View File

@@ -33,6 +33,8 @@ export interface IKeybindingEditingService {
readonly _serviceBrand: undefined;
addKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;
editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void>;
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
@@ -58,8 +60,12 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
this.queue = new Queue<void>();
}
addKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {
return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when, true)); // queue up writes to prevent race conditions
}
editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {
return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when)); // queue up writes to prevent race conditions
return this.queue.queue(() => this.doEditKeybinding(keybindingItem, key, when, false)); // queue up writes to prevent race conditions
}
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
@@ -70,18 +76,24 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions
}
private doEditKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise<void> {
return this.resolveAndValidate()
.then(reference => {
const model = reference.object.textEditorModel;
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
this.updateKeybinding(keybindingItem, key, when, model, userKeybindingEntryIndex);
if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {
this.removeDefaultKeybinding(keybindingItem, model);
}
return this.save().finally(() => reference.dispose());
});
private async doEditKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined, add: boolean): Promise<void> {
const reference = await this.resolveAndValidate();
const model = reference.object.textEditorModel;
if (add) {
this.updateKeybinding(keybindingItem, key, when, model, -1);
} else {
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
this.updateKeybinding(keybindingItem, key, when, model, userKeybindingEntryIndex);
if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {
this.removeDefaultKeybinding(keybindingItem, model);
}
}
try {
await this.save();
} finally {
reference.dispose();
}
}
private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {

View File

@@ -1,49 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { ScanCodeBinding } from 'vs/base/common/scanCode';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
export interface IKeyboardMapper {
dumpDebugInfo(): string;
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
resolveUserBinding(firstPart: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[];
}
export class CachedKeyboardMapper implements IKeyboardMapper {
private _actual: IKeyboardMapper;
private _cache: Map<string, ResolvedKeybinding[]>;
constructor(actual: IKeyboardMapper) {
this._actual = actual;
this._cache = new Map<string, ResolvedKeybinding[]>();
}
public dumpDebugInfo(): string {
return this._actual.dumpDebugInfo();
}
public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
const hashCode = keybinding.getHashCode();
const resolved = this._cache.get(hashCode);
if (!resolved) {
const r = this._actual.resolveKeybinding(keybinding);
this._cache.set(hashCode, r);
return r;
}
return resolved;
}
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
return this._actual.resolveKeyboardEvent(keyboardEvent);
}
public resolveUserBinding(parts: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[] {
return this._actual.resolveUserBinding(parts);
}
}

View File

@@ -3,191 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { DispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
export interface IWindowsKeyMapping {
vkey: string;
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
export interface IWindowsKeyboardMapping {
[code: string]: IWindowsKeyMapping;
}
export interface ILinuxKeyMapping {
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
export interface ILinuxKeyboardMapping {
[code: string]: ILinuxKeyMapping;
}
export interface IMacKeyMapping {
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
valueIsDeadKey: boolean;
withShiftIsDeadKey: boolean;
withAltGrIsDeadKey: boolean;
withShiftAltGrIsDeadKey: boolean;
}
export interface IMacKeyboardMapping {
[code: string]: IMacKeyMapping;
}
export type IKeyboardMapping = IWindowsKeyboardMapping | ILinuxKeyboardMapping | IMacKeyboardMapping;
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"id": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"text": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface IWindowsKeyboardLayoutInfo {
name: string;
id: string;
text: string;
}
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"model" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"layout": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"variant": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"options": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"rules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface ILinuxKeyboardLayoutInfo {
model: string;
layout: string;
variant: string;
options: string;
rules: string;
}
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"lang": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"localizedName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface IMacKeyboardLayoutInfo {
id: string;
lang: string;
localizedName?: string;
}
export type IKeyboardLayoutInfo = (IWindowsKeyboardLayoutInfo | ILinuxKeyboardLayoutInfo | IMacKeyboardLayoutInfo) & { isUserKeyboardLayout?: boolean; isUSStandard?: true };
export const IKeymapService = createDecorator<IKeymapService>('keymapService');
export interface IKeymapService {
readonly _serviceBrand: undefined;
onDidChangeKeyboardMapper: Event<void>;
getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper;
getCurrentKeyboardLayout(): IKeyboardLayoutInfo | null;
getAllKeyboardLayouts(): IKeyboardLayoutInfo[];
getRawKeyboardMapping(): IKeyboardMapping | null;
validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void;
}
export function areKeyboardLayoutsEqual(a: IKeyboardLayoutInfo | null, b: IKeyboardLayoutInfo | null): boolean {
if (!a || !b) {
return false;
}
if ((<IWindowsKeyboardLayoutInfo>a).name && (<IWindowsKeyboardLayoutInfo>b).name && (<IWindowsKeyboardLayoutInfo>a).name === (<IWindowsKeyboardLayoutInfo>b).name) {
return true;
}
if ((<IMacKeyboardLayoutInfo>a).id && (<IMacKeyboardLayoutInfo>b).id && (<IMacKeyboardLayoutInfo>a).id === (<IMacKeyboardLayoutInfo>b).id) {
return true;
}
if ((<ILinuxKeyboardLayoutInfo>a).model &&
(<ILinuxKeyboardLayoutInfo>b).model &&
(<ILinuxKeyboardLayoutInfo>a).model === (<ILinuxKeyboardLayoutInfo>b).model &&
(<ILinuxKeyboardLayoutInfo>a).layout === (<ILinuxKeyboardLayoutInfo>b).layout
) {
return true;
}
return false;
}
export function parseKeyboardLayoutDescription(layout: IKeyboardLayoutInfo | null): { label: string, description: string } {
if (!layout) {
return { label: '', description: '' };
}
if ((<IWindowsKeyboardLayoutInfo>layout).name) {
// windows
let windowsLayout = <IWindowsKeyboardLayoutInfo>layout;
return {
label: windowsLayout.text,
description: ''
};
}
if ((<IMacKeyboardLayoutInfo>layout).id) {
let macLayout = <IMacKeyboardLayoutInfo>layout;
if (macLayout.localizedName) {
return {
label: macLayout.localizedName,
description: ''
};
}
if (/^com\.apple\.keylayout\./.test(macLayout.id)) {
return {
label: macLayout.id.replace(/^com\.apple\.keylayout\./, '').replace(/-/, ' '),
description: ''
};
}
if (/^.*inputmethod\./.test(macLayout.id)) {
return {
label: macLayout.id.replace(/^.*inputmethod\./, '').replace(/[-\.]/, ' '),
description: `Input Method (${macLayout.lang})`
};
}
return {
label: macLayout.lang,
description: ''
};
}
let linuxLayout = <ILinuxKeyboardLayoutInfo>layout;
return {
label: linuxLayout.layout,
description: ''
};
}
export function getKeyboardLayoutId(layout: IKeyboardLayoutInfo): string {
if ((<IWindowsKeyboardLayoutInfo>layout).name) {
return (<IWindowsKeyboardLayoutInfo>layout).name;
}
if ((<IMacKeyboardLayoutInfo>layout).id) {
return (<IMacKeyboardLayoutInfo>layout).id;
}
return (<ILinuxKeyboardLayoutInfo>layout).layout;
}
import { getKeyboardLayoutId, IKeyboardLayoutInfo } from 'vs/platform/keyboardLayout/common/keyboardLayout';
function deserializeMapping(serializedMapping: ISerializedMapping) {
let mapping = serializedMapping;

View File

@@ -8,7 +8,7 @@ import { OperatingSystem } from 'vs/base/common/platform';
import { IMMUTABLE_CODE_TO_KEY_CODE, ScanCode, ScanCodeBinding } from 'vs/base/common/scanCode';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { removeElementsAfterNulls } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
/**

View File

@@ -8,52 +8,9 @@ import { KeyCode, KeyCodeUtils, Keybinding, ResolvedKeybinding, SimpleKeybinding
import { OperatingSystem } from 'vs/base/common/platform';
import { IMMUTABLE_CODE_TO_KEY_CODE, IMMUTABLE_KEY_CODE_TO_CODE, ScanCode, ScanCodeBinding, ScanCodeUtils } from 'vs/base/common/scanCode';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { BaseResolvedKeybinding } from 'vs/platform/keybinding/common/baseResolvedKeybinding';
export interface IMacLinuxKeyMapping {
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
function macLinuxKeyMappingEquals(a: IMacLinuxKeyMapping, b: IMacLinuxKeyMapping): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.value === b.value
&& a.withShift === b.withShift
&& a.withAltGr === b.withAltGr
&& a.withShiftAltGr === b.withShiftAltGr
);
}
export interface IMacLinuxKeyboardMapping {
[scanCode: string]: IMacLinuxKeyMapping;
}
export function macLinuxKeyboardMappingEquals(a: IMacLinuxKeyboardMapping | null, b: IMacLinuxKeyboardMapping | null): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
for (let scanCode = 0; scanCode < ScanCode.MAX_VALUE; scanCode++) {
const strScanCode = ScanCodeUtils.toString(scanCode);
const aEntry = a[strScanCode];
const bEntry = b[strScanCode];
if (!macLinuxKeyMappingEquals(aEntry, bEntry)) {
return false;
}
}
return true;
}
import { IMacLinuxKeyboardMapping, IMacLinuxKeyMapping } from 'vs/platform/keyboardLayout/common/keyboardLayout';
/**
* A map from character to key codes.

View File

@@ -9,56 +9,10 @@ import { UILabelProvider } from 'vs/base/common/keybindingLabels';
import { OperatingSystem } from 'vs/base/common/platform';
import { IMMUTABLE_CODE_TO_KEY_CODE, ScanCode, ScanCodeBinding, ScanCodeUtils } from 'vs/base/common/scanCode';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { BaseResolvedKeybinding } from 'vs/platform/keybinding/common/baseResolvedKeybinding';
import { removeElementsAfterNulls } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
export interface IWindowsKeyMapping {
vkey: string;
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
function windowsKeyMappingEquals(a: IWindowsKeyMapping, b: IWindowsKeyMapping): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.vkey === b.vkey
&& a.value === b.value
&& a.withShift === b.withShift
&& a.withAltGr === b.withAltGr
&& a.withShiftAltGr === b.withShiftAltGr
);
}
export interface IWindowsKeyboardMapping {
[scanCode: string]: IWindowsKeyMapping;
}
export function windowsKeyboardMappingEquals(a: IWindowsKeyboardMapping | null, b: IWindowsKeyboardMapping | null): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
for (let scanCode = 0; scanCode < ScanCode.MAX_VALUE; scanCode++) {
const strScanCode = ScanCodeUtils.toString(scanCode);
const aEntry = a[strScanCode];
const bEntry = b[strScanCode];
if (!windowsKeyMappingEquals(aEntry, bEntry)) {
return false;
}
}
return true;
}
import { IWindowsKeyboardMapping } from 'vs/platform/keyboardLayout/common/keyboardLayout';
const LOG = false;
function log(str: string): void {

View File

@@ -1,174 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nativeKeymap from 'native-keymap';
import { Disposable } from 'vs/base/common/lifecycle';
import { IKeymapService, IKeyboardLayoutInfo, IKeyboardMapping } from 'vs/workbench/services/keybinding/common/keymapInfo';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IKeyboardMapper, CachedKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { Emitter, Event } from 'vs/base/common/event';
import { DispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig';
import { MacLinuxFallbackKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { WindowsKeyboardMapper, windowsKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
import { MacLinuxKeyboardMapper, macLinuxKeyboardMappingEquals, IMacLinuxKeyboardMapping } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
export class KeyboardMapperFactory {
public static readonly INSTANCE = new KeyboardMapperFactory();
private _layoutInfo: nativeKeymap.IKeyboardLayoutInfo | null;
private _rawMapping: nativeKeymap.IKeyboardMapping | null;
private _keyboardMapper: IKeyboardMapper | null;
private _initialized: boolean;
private readonly _onDidChangeKeyboardMapper = new Emitter<void>();
public readonly onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
private constructor() {
this._layoutInfo = null;
this._rawMapping = null;
this._keyboardMapper = null;
this._initialized = false;
}
public _onKeyboardLayoutChanged(): void {
if (this._initialized) {
this._setKeyboardData(nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
}
public getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper {
if (!this._initialized) {
this._setKeyboardData(nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
if (dispatchConfig === DispatchConfig.KeyCode) {
// Forcefully set to use keyCode
return new MacLinuxFallbackKeyboardMapper(OS);
}
return this._keyboardMapper!;
}
public getCurrentKeyboardLayout(): nativeKeymap.IKeyboardLayoutInfo | null {
if (!this._initialized) {
this._setKeyboardData(nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
return this._layoutInfo;
}
private static _isUSStandard(_kbInfo: nativeKeymap.IKeyboardLayoutInfo): boolean {
if (OS === OperatingSystem.Linux) {
const kbInfo = <nativeKeymap.ILinuxKeyboardLayoutInfo>_kbInfo;
return (kbInfo && (kbInfo.layout === 'us' || /^us,/.test(kbInfo.layout)));
}
if (OS === OperatingSystem.Macintosh) {
const kbInfo = <nativeKeymap.IMacKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.id === 'com.apple.keylayout.US');
}
if (OS === OperatingSystem.Windows) {
const kbInfo = <nativeKeymap.IWindowsKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.name === '00000409');
}
return false;
}
public getRawKeyboardMapping(): nativeKeymap.IKeyboardMapping | null {
if (!this._initialized) {
this._setKeyboardData(nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
return this._rawMapping;
}
private _setKeyboardData(layoutInfo: nativeKeymap.IKeyboardLayoutInfo, rawMapping: nativeKeymap.IKeyboardMapping): void {
this._layoutInfo = layoutInfo;
if (this._initialized && KeyboardMapperFactory._equals(this._rawMapping, rawMapping)) {
// nothing to do...
return;
}
this._initialized = true;
this._rawMapping = rawMapping;
this._keyboardMapper = new CachedKeyboardMapper(
KeyboardMapperFactory._createKeyboardMapper(this._layoutInfo, this._rawMapping)
);
this._onDidChangeKeyboardMapper.fire();
}
private static _createKeyboardMapper(layoutInfo: nativeKeymap.IKeyboardLayoutInfo, rawMapping: nativeKeymap.IKeyboardMapping): IKeyboardMapper {
const isUSStandard = KeyboardMapperFactory._isUSStandard(layoutInfo);
if (OS === OperatingSystem.Windows) {
return new WindowsKeyboardMapper(isUSStandard, <nativeKeymap.IWindowsKeyboardMapping>rawMapping);
}
if (Object.keys(rawMapping).length === 0) {
// Looks like reading the mappings failed (most likely Mac + Japanese/Chinese keyboard layouts)
return new MacLinuxFallbackKeyboardMapper(OS);
}
if (OS === OperatingSystem.Macintosh) {
const kbInfo = <nativeKeymap.IMacKeyboardLayoutInfo>layoutInfo;
if (kbInfo.id === 'com.apple.keylayout.DVORAK-QWERTYCMD') {
// Use keyCode based dispatching for DVORAK - QWERTY ⌘
return new MacLinuxFallbackKeyboardMapper(OS);
}
}
return new MacLinuxKeyboardMapper(isUSStandard, <IMacLinuxKeyboardMapping>rawMapping, OS);
}
private static _equals(a: nativeKeymap.IKeyboardMapping | null, b: nativeKeymap.IKeyboardMapping | null): boolean {
if (OS === OperatingSystem.Windows) {
return windowsKeyboardMappingEquals(<nativeKeymap.IWindowsKeyboardMapping>a, <nativeKeymap.IWindowsKeyboardMapping>b);
}
return macLinuxKeyboardMappingEquals(<IMacLinuxKeyboardMapping>a, <IMacLinuxKeyboardMapping>b);
}
}
class NativeKeymapService extends Disposable implements IKeymapService {
public _serviceBrand: undefined;
private readonly _onDidChangeKeyboardMapper = this._register(new Emitter<void>());
public readonly onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
constructor() {
super();
this._register(KeyboardMapperFactory.INSTANCE.onDidChangeKeyboardMapper(() => {
this._onDidChangeKeyboardMapper.fire();
}));
ipcRenderer.on('vscode:keyboardLayoutChanged', () => {
KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged();
});
}
getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper {
return KeyboardMapperFactory.INSTANCE.getKeyboardMapper(dispatchConfig);
}
public getCurrentKeyboardLayout(): IKeyboardLayoutInfo | null {
return KeyboardMapperFactory.INSTANCE.getCurrentKeyboardLayout();
}
getAllKeyboardLayouts(): IKeyboardLayoutInfo[] {
return [];
}
public getRawKeyboardMapping(): IKeyboardMapping | null {
return KeyboardMapperFactory.INSTANCE.getRawKeyboardMapping();
}
public validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void {
return;
}
}
registerSingleton(IKeymapService, NativeKeymapService, true);

View File

@@ -0,0 +1,144 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IKeyboardLayoutInfo, IKeyboardLayoutService, IKeyboardMapping, ILinuxKeyboardLayoutInfo, IMacKeyboardLayoutInfo, IMacLinuxKeyboardMapping, IWindowsKeyboardLayoutInfo, IWindowsKeyboardMapping, macLinuxKeyboardMappingEquals, windowsKeyboardMappingEquals } from 'vs/platform/keyboardLayout/common/keyboardLayout';
import { Emitter } from 'vs/base/common/event';
import { OperatingSystem, OS } from 'vs/base/common/platform';
import { CachedKeyboardMapper, IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { WindowsKeyboardMapper } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
import { MacLinuxFallbackKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper';
import { MacLinuxKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { DispatchConfig } from 'vs/platform/keyboardLayout/common/dispatchConfig';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { IKeyboardLayoutMainService } from 'vs/platform/keyboardLayout/common/keyboardLayoutMainService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
export class KeyboardLayoutService extends Disposable implements IKeyboardLayoutService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeKeyboardLayout = this._register(new Emitter<void>());
readonly onDidChangeKeyboardLayout = this._onDidChangeKeyboardLayout.event;
private readonly _keyboardLayoutMainService: IKeyboardLayoutMainService;
private _initPromise: Promise<void> | null;
private _keyboardMapping: IKeyboardMapping | null;
private _keyboardLayoutInfo: IKeyboardLayoutInfo | null;
private _keyboardMapper: IKeyboardMapper;
constructor(
@IMainProcessService mainProcessService: IMainProcessService
) {
super();
this._keyboardLayoutMainService = createChannelSender<IKeyboardLayoutMainService>(mainProcessService.getChannel('keyboardLayout'));
this._initPromise = null;
this._keyboardMapping = null;
this._keyboardLayoutInfo = null;
this._keyboardMapper = new MacLinuxFallbackKeyboardMapper(OS);
this._register(this._keyboardLayoutMainService.onDidChangeKeyboardLayout(async ({ keyboardLayoutInfo, keyboardMapping }) => {
await this.initialize();
if (keyboardMappingEquals(this._keyboardMapping, keyboardMapping)) {
// the mappings are equal
return;
}
this._keyboardMapping = keyboardMapping;
this._keyboardLayoutInfo = keyboardLayoutInfo;
this._keyboardMapper = new CachedKeyboardMapper(createKeyboardMapper(this._keyboardLayoutInfo, this._keyboardMapping));
this._onDidChangeKeyboardLayout.fire();
}));
}
public initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
const keyboardLayoutData = await this._keyboardLayoutMainService.getKeyboardLayoutData();
const { keyboardLayoutInfo, keyboardMapping } = keyboardLayoutData;
this._keyboardMapping = keyboardMapping;
this._keyboardLayoutInfo = keyboardLayoutInfo;
this._keyboardMapper = new CachedKeyboardMapper(createKeyboardMapper(this._keyboardLayoutInfo, this._keyboardMapping));
}
public getRawKeyboardMapping(): IKeyboardMapping | null {
return this._keyboardMapping;
}
public getCurrentKeyboardLayout(): IKeyboardLayoutInfo | null {
return this._keyboardLayoutInfo;
}
public getAllKeyboardLayouts(): IKeyboardLayoutInfo[] {
return [];
}
public getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper {
if (dispatchConfig === DispatchConfig.KeyCode) {
// Forcefully set to use keyCode
return new MacLinuxFallbackKeyboardMapper(OS);
}
return this._keyboardMapper;
}
public validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void {
return;
}
}
function keyboardMappingEquals(a: IKeyboardMapping | null, b: IKeyboardMapping | null): boolean {
if (OS === OperatingSystem.Windows) {
return windowsKeyboardMappingEquals(<IWindowsKeyboardMapping | null>a, <IWindowsKeyboardMapping | null>b);
}
return macLinuxKeyboardMappingEquals(<IMacLinuxKeyboardMapping | null>a, <IMacLinuxKeyboardMapping | null>b);
}
function createKeyboardMapper(layoutInfo: IKeyboardLayoutInfo | null, rawMapping: IKeyboardMapping | null): IKeyboardMapper {
const _isUSStandard = isUSStandard(layoutInfo);
if (OS === OperatingSystem.Windows) {
return new WindowsKeyboardMapper(_isUSStandard, <IWindowsKeyboardMapping>rawMapping);
}
if (!rawMapping || Object.keys(rawMapping).length === 0) {
// Looks like reading the mappings failed (most likely Mac + Japanese/Chinese keyboard layouts)
return new MacLinuxFallbackKeyboardMapper(OS);
}
if (OS === OperatingSystem.Macintosh) {
const kbInfo = <IMacKeyboardLayoutInfo>layoutInfo;
if (kbInfo.id === 'com.apple.keylayout.DVORAK-QWERTYCMD') {
// Use keyCode based dispatching for DVORAK - QWERTY ⌘
return new MacLinuxFallbackKeyboardMapper(OS);
}
}
return new MacLinuxKeyboardMapper(_isUSStandard, <IMacLinuxKeyboardMapping>rawMapping, OS);
}
function isUSStandard(_kbInfo: IKeyboardLayoutInfo | null): boolean {
if (OS === OperatingSystem.Linux) {
const kbInfo = <ILinuxKeyboardLayoutInfo>_kbInfo;
return (kbInfo && (kbInfo.layout === 'us' || /^us,/.test(kbInfo.layout)));
}
if (OS === OperatingSystem.Macintosh) {
const kbInfo = <IMacKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.id === 'com.apple.keylayout.US');
}
if (OS === OperatingSystem.Windows) {
const kbInfo = <IWindowsKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.name === '00000409');
}
return false;
}

View File

@@ -6,7 +6,7 @@ import * as assert from 'assert';
import 'vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin'; // 15%
import 'vs/workbench/services/keybinding/browser/keyboardLayouts/de.darwin';
import { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution';
import { BrowserKeyboardMapperFactoryBase } from 'vs/workbench/services/keybinding/browser/keymapService';
import { BrowserKeyboardMapperFactoryBase } from 'vs/workbench/services/keybinding/browser/keyboardLayoutService';
import { KeymapInfo, IKeymapInfo } from 'vs/workbench/services/keybinding/common/keymapInfo';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { INotificationService } from 'vs/platform/notification/common/notification';

View File

@@ -117,7 +117,7 @@ suite('KeybindingsEditing', () => {
const fileService = new FileService(new NullLogService());
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, new NullLogService()));
instantiationService.stub(IFileService, fileService);
instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService));
instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService());
@@ -194,7 +194,19 @@ suite('KeybindingsEditing', () => {
.then(() => assert.deepEqual(getUserKeybindings(), expected));
});
test('add another keybinding', () => {
const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }];
return testObject.addKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape }, command: 'a' }), 'alt+c', undefined)
.then(() => assert.deepEqual(getUserKeybindings(), expected));
});
test('add a new default keybinding', () => {
const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }];
return testObject.addKeybinding(aResolvedKeybindingItem({ command: 'a' }), 'alt+c', undefined)
.then(() => assert.deepEqual(getUserKeybindings(), expected));
});
test('add a new default keybinding using edit', () => {
const expected: IUserFriendlyKeybinding[] = [{ key: 'alt+c', command: 'a' }];
return testObject.editKeybinding(aResolvedKeybindingItem({ command: 'a' }), 'alt+c', undefined)
.then(() => assert.deepEqual(getUserKeybindings(), expected));
@@ -312,7 +324,7 @@ suite('KeybindingsEditing', () => {
}
}
const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined;
return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null);
return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null, false);
}
});

View File

@@ -10,7 +10,7 @@ import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common
import { ScanCodeBinding } from 'vs/base/common/scanCode';
import { readFile, writeFile } from 'vs/base/node/pfs';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
export interface IResolvedKeybinding {
label: string | null;

View File

@@ -9,8 +9,9 @@ import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels';
import { OperatingSystem } from 'vs/base/common/platform';
import { ScanCode, ScanCodeBinding, ScanCodeUtils } from 'vs/base/common/scanCode';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { IMacLinuxKeyboardMapping, MacLinuxKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { MacLinuxKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { IResolvedKeybinding, assertMapping, assertResolveKeybinding, assertResolveKeyboardEvent, assertResolveUserBinding, readRawMapping } from 'vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils';
import { IMacLinuxKeyboardMapping } from 'vs/platform/keyboardLayout/common/keyboardLayout';
const WRITE_FILE_IF_DIFFERENT = false;

View File

@@ -6,8 +6,9 @@
import { KeyChord, KeyCode, KeyMod, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes';
import { OperatingSystem } from 'vs/base/common/platform';
import { ScanCode, ScanCodeBinding } from 'vs/base/common/scanCode';
import { IWindowsKeyboardMapping, WindowsKeyboardMapper } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
import { WindowsKeyboardMapper } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
import { IResolvedKeybinding, assertMapping, assertResolveKeybinding, assertResolveKeyboardEvent, assertResolveUserBinding, readRawMapping } from 'vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils';
import { IWindowsKeyboardMapping } from 'vs/platform/keyboardLayout/common/keyboardLayout';
const WRITE_FILE_IF_DIFFERENT = false;

View File

@@ -9,7 +9,8 @@ import { TestEnvironmentService, TestPathService } from 'vs/workbench/test/brows
import { URI } from 'vs/base/common/uri';
import { LabelService } from 'vs/workbench/services/label/common/labelService';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace';
suite('URI Label', () => {
let labelService: LabelService;

View File

@@ -7,7 +7,7 @@ import { localize } from 'vs/nls';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle';
import { ShutdownReason, StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -95,7 +95,7 @@ export class NativeLifecycleService extends AbstractLifecycleService {
// Save shutdown reason to retrieve on next startup
this.storageService.onWillSaveState(e => {
if (e.reason === WillSaveStateReason.SHUTDOWN) {
this.storageService.store(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, this.shutdownReason, StorageScope.WORKSPACE);
this.storageService.store(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, this.shutdownReason, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}
});
}

View File

@@ -5,19 +5,18 @@
import * as nls from 'vs/nls';
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions, IStatusMessageOptions, NoOpNotification, NeverShowAgainScope, NotificationsFilter } from 'vs/platform/notification/common/notification';
import { INotificationsModel, NotificationsModel, ChoiceAction } from 'vs/workbench/common/notifications';
import { NotificationsModel, ChoiceAction } from 'vs/workbench/common/notifications';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IAction, Action } from 'vs/base/common/actions';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
export class NotificationService extends Disposable implements INotificationService {
declare readonly _serviceBrand: undefined;
private _model: INotificationsModel = this._register(new NotificationsModel());
get model(): INotificationsModel { return this._model; }
readonly model = this._register(new NotificationsModel());
constructor(
@IStorageService private readonly storageService: IStorageService
@@ -26,7 +25,7 @@ export class NotificationService extends Disposable implements INotificationServ
}
setFilter(filter: NotificationsFilter): void {
this._model.setFilter(filter);
this.model.setFilter(filter);
}
info(message: NotificationMessage | NotificationMessage[]): void {
@@ -83,7 +82,7 @@ export class NotificationService extends Disposable implements INotificationServ
handle.close();
// Remember choice
this.storageService.store(id, true, scope);
this.storageService.store(id, true, scope, StorageTarget.USER);
return Promise.resolve();
}));
@@ -127,7 +126,7 @@ export class NotificationService extends Disposable implements INotificationServ
const neverShowAgainChoice = {
label: nls.localize('neverShowAgain', "Don't Show Again"),
run: () => this.storageService.store(id, true, scope),
run: () => this.storageService.store(id, true, scope, StorageTarget.USER),
isSecondary: options.neverShowAgain.isSecondary
};

View File

@@ -1,428 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import * as strings from 'vs/base/common/strings';
import { ITextModel } from 'vs/editor/common/model';
import { Emitter, Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { RunOnceScheduler, ThrottledDelayer } from 'vs/base/common/async';
import { IFileService } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { Disposable, toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { isNumber } from 'vs/base/common/types';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { binarySearch } from 'vs/base/common/arrays';
import { VSBuffer } from 'vs/base/common/buffer';
export interface IOutputChannelModel extends IDisposable {
readonly onDidAppendedContent: Event<void>;
readonly onDispose: Event<void>;
append(output: string): void;
update(): void;
loadModel(): Promise<ITextModel>;
clear(till?: number): void;
}
export const IOutputChannelModelService = createDecorator<IOutputChannelModelService>('outputChannelModelService');
export interface IOutputChannelModelService {
readonly _serviceBrand: undefined;
createOutputChannelModel(id: string, modelUri: URI, mimeType: string, file?: URI): IOutputChannelModel;
}
export abstract class AsbtractOutputChannelModelService {
constructor(
@IInstantiationService protected readonly instantiationService: IInstantiationService
) { }
createOutputChannelModel(id: string, modelUri: URI, mimeType: string, file?: URI): IOutputChannelModel {
return file ? this.instantiationService.createInstance(FileOutputChannelModel, modelUri, mimeType, file) : this.instantiationService.createInstance(BufferredOutputChannel, modelUri, mimeType);
}
}
export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel {
protected readonly _onDidAppendedContent = this._register(new Emitter<void>());
readonly onDidAppendedContent: Event<void> = this._onDidAppendedContent.event;
protected readonly _onDispose = this._register(new Emitter<void>());
readonly onDispose: Event<void> = this._onDispose.event;
protected modelUpdater: RunOnceScheduler;
protected model: ITextModel | null = null;
protected startOffset: number = 0;
protected endOffset: number = 0;
constructor(
private readonly modelUri: URI,
private readonly mimeType: string,
protected readonly file: URI,
protected fileService: IFileService,
protected modelService: IModelService,
protected modeService: IModeService,
) {
super();
this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300);
this._register(toDisposable(() => this.modelUpdater.cancel()));
}
clear(till?: number): void {
if (this.modelUpdater.isScheduled()) {
this.modelUpdater.cancel();
this.onUpdateModelCancelled();
}
if (this.model) {
this.model.setValue('');
}
this.endOffset = isNumber(till) ? till : this.endOffset;
this.startOffset = this.endOffset;
}
update(): void { }
protected createModel(content: string): ITextModel {
if (this.model) {
this.model.setValue(content);
} else {
this.model = this.modelService.createModel(content, this.modeService.create(this.mimeType), this.modelUri);
this.onModelCreated(this.model);
const disposable = this.model.onWillDispose(() => {
this.onModelWillDispose(this.model);
this.model = null;
dispose(disposable);
});
}
return this.model;
}
appendToModel(content: string): void {
if (this.model && content) {
const lastLine = this.model.getLineCount();
const lastLineMaxColumn = this.model.getLineMaxColumn(lastLine);
this.model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), content)]);
this._onDidAppendedContent.fire();
}
}
abstract loadModel(): Promise<ITextModel>;
abstract append(message: string): void;
protected onModelCreated(model: ITextModel) { }
protected onModelWillDispose(model: ITextModel | null) { }
protected onUpdateModelCancelled() { }
protected updateModel() { }
dispose(): void {
this._onDispose.fire();
super.dispose();
}
}
// TODO@ben see if new watchers can cope with spdlog and avoid polling then
class OutputFileListener extends Disposable {
private readonly _onDidContentChange = new Emitter<number | undefined>();
readonly onDidContentChange: Event<number | undefined> = this._onDidContentChange.event;
private watching: boolean = false;
private syncDelayer: ThrottledDelayer<void>;
private etag: string | undefined;
constructor(
private readonly file: URI,
private readonly fileService: IFileService
) {
super();
this.syncDelayer = new ThrottledDelayer<void>(500);
}
watch(eTag: string | undefined): void {
if (!this.watching) {
this.etag = eTag;
this.poll();
this.watching = true;
}
}
private poll(): void {
const loop = () => this.doWatch().then(() => this.poll());
this.syncDelayer.trigger(loop);
}
private doWatch(): Promise<void> {
return this.fileService.resolve(this.file, { resolveMetadata: true })
.then(stat => {
if (stat.etag !== this.etag) {
this.etag = stat.etag;
this._onDidContentChange.fire(stat.size);
}
});
}
unwatch(): void {
if (this.watching) {
this.syncDelayer.cancel();
this.watching = false;
}
}
dispose(): void {
this.unwatch();
super.dispose();
}
}
/**
* An output channel driven by a file and does not support appending messages.
*/
class FileOutputChannelModel extends AbstractFileOutputChannelModel implements IOutputChannelModel {
private readonly fileHandler: OutputFileListener;
private updateInProgress: boolean = false;
private etag: string | undefined = '';
private loadModelPromise: Promise<ITextModel> | null = null;
constructor(
modelUri: URI,
mimeType: string,
file: URI,
@IFileService fileService: IFileService,
@IModelService modelService: IModelService,
@IModeService modeService: IModeService
) {
super(modelUri, mimeType, file, fileService, modelService, modeService);
this.fileHandler = this._register(new OutputFileListener(this.file, this.fileService));
this._register(this.fileHandler.onDidContentChange(size => this.update(size)));
this._register(toDisposable(() => this.fileHandler.unwatch()));
}
loadModel(): Promise<ITextModel> {
this.loadModelPromise = new Promise<ITextModel>(async (c, e) => {
try {
let content = '';
if (await this.fileService.exists(this.file)) {
const fileContent = await this.fileService.readFile(this.file, { position: this.startOffset });
this.endOffset = this.startOffset + fileContent.value.byteLength;
this.etag = fileContent.etag;
content = fileContent.value.toString();
} else {
this.startOffset = 0;
this.endOffset = 0;
}
c(this.createModel(content));
} catch (error) {
e(error);
}
});
return this.loadModelPromise;
}
clear(till?: number): void {
const loadModelPromise: Promise<any> = this.loadModelPromise ? this.loadModelPromise : Promise.resolve();
loadModelPromise.then(() => {
super.clear(till);
this.update();
});
}
append(message: string): void {
throw new Error('Not supported');
}
protected updateModel(): void {
if (this.model) {
this.fileService.readFile(this.file, { position: this.endOffset })
.then(content => {
this.etag = content.etag;
if (content.value) {
this.endOffset = this.endOffset + content.value.byteLength;
this.appendToModel(content.value.toString());
}
this.updateInProgress = false;
}, () => this.updateInProgress = false);
} else {
this.updateInProgress = false;
}
}
protected onModelCreated(model: ITextModel): void {
this.fileHandler.watch(this.etag);
}
protected onModelWillDispose(model: ITextModel | null): void {
this.fileHandler.unwatch();
}
protected onUpdateModelCancelled(): void {
this.updateInProgress = false;
}
protected getByteLength(str: string): number {
return VSBuffer.fromString(str).byteLength;
}
update(size?: number): void {
if (this.model) {
if (!this.updateInProgress) {
this.updateInProgress = true;
if (isNumber(size) && this.endOffset > size) { // Reset - Content is removed
this.startOffset = this.endOffset = 0;
this.model.setValue('');
}
this.modelUpdater.schedule();
}
}
}
}
export class BufferredOutputChannel extends Disposable implements IOutputChannelModel {
readonly file: URI | null = null;
scrollLock: boolean = false;
protected _onDidAppendedContent = new Emitter<void>();
readonly onDidAppendedContent: Event<void> = this._onDidAppendedContent.event;
private readonly _onDispose = new Emitter<void>();
readonly onDispose: Event<void> = this._onDispose.event;
private modelUpdater: RunOnceScheduler;
private model: ITextModel | null = null;
private readonly bufferredContent: BufferedContent;
private lastReadId: number | undefined = undefined;
constructor(
private readonly modelUri: URI, private readonly mimeType: string,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService
) {
super();
this.modelUpdater = new RunOnceScheduler(() => this.updateModel(), 300);
this._register(toDisposable(() => this.modelUpdater.cancel()));
this.bufferredContent = new BufferedContent();
this._register(toDisposable(() => this.bufferredContent.clear()));
}
append(output: string) {
this.bufferredContent.append(output);
if (!this.modelUpdater.isScheduled()) {
this.modelUpdater.schedule();
}
}
update(): void { }
clear(): void {
if (this.modelUpdater.isScheduled()) {
this.modelUpdater.cancel();
}
if (this.model) {
this.model.setValue('');
}
this.bufferredContent.clear();
this.lastReadId = undefined;
}
loadModel(): Promise<ITextModel> {
const { value, id } = this.bufferredContent.getDelta(this.lastReadId);
if (this.model) {
this.model.setValue(value);
} else {
this.model = this.createModel(value);
}
this.lastReadId = id;
return Promise.resolve(this.model);
}
private createModel(content: string): ITextModel {
const model = this.modelService.createModel(content, this.modeService.create(this.mimeType), this.modelUri);
const disposable = model.onWillDispose(() => {
this.model = null;
dispose(disposable);
});
return model;
}
private updateModel(): void {
if (this.model) {
const { value, id } = this.bufferredContent.getDelta(this.lastReadId);
this.lastReadId = id;
const lastLine = this.model.getLineCount();
const lastLineMaxColumn = this.model.getLineMaxColumn(lastLine);
this.model.applyEdits([EditOperation.insert(new Position(lastLine, lastLineMaxColumn), value)]);
this._onDidAppendedContent.fire();
}
}
dispose(): void {
this._onDispose.fire();
super.dispose();
}
}
class BufferedContent {
private static readonly MAX_OUTPUT_LENGTH = 10000 /* Max. number of output lines to show in output */ * 100 /* Guestimated chars per line */;
private data: string[] = [];
private dataIds: number[] = [];
private idPool = 0;
private length = 0;
public append(content: string): void {
this.data.push(content);
this.dataIds.push(++this.idPool);
this.length += content.length;
this.trim();
}
public clear(): void {
this.data.length = 0;
this.dataIds.length = 0;
this.length = 0;
}
private trim(): void {
if (this.length < BufferedContent.MAX_OUTPUT_LENGTH * 1.2) {
return;
}
while (this.length > BufferedContent.MAX_OUTPUT_LENGTH) {
this.dataIds.shift();
const removed = this.data.shift();
if (removed) {
this.length -= removed.length;
}
}
}
public getDelta(previousId?: number): { value: string, id: number } {
let idx = -1;
if (previousId !== undefined) {
idx = binarySearch(this.dataIds, previousId, (a, b) => a - b);
}
const id = this.idPool;
if (idx >= 0) {
const value = strings.removeAnsiEscapeCodes(this.data.slice(idx + 1).join(''));
return { value, id };
} else {
const value = strings.removeAnsiEscapeCodes(this.data.join(''));
return { value, id };
}
}
}

View File

@@ -1,229 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { join } from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { ITextModel } from 'vs/editor/common/model';
import { URI } from 'vs/base/common/uri';
import { ThrottledDelayer } from 'vs/base/common/async';
import { IFileService } from 'vs/platform/files/common/files';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IOutputChannelModel, AbstractFileOutputChannelModel, IOutputChannelModelService, AsbtractOutputChannelModelService, BufferredOutputChannel } from 'vs/workbench/services/output/common/outputChannelModel';
import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { toLocalISOString } from 'vs/base/common/date';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implements IOutputChannelModel {
private appender: OutputAppender;
private appendedMessage: string;
private loadingFromFileInProgress: boolean;
private resettingDelayer: ThrottledDelayer<void>;
private readonly rotatingFilePath: URI;
constructor(
id: string,
modelUri: URI,
mimeType: string,
file: URI,
@IFileService fileService: IFileService,
@IModelService modelService: IModelService,
@IModeService modeService: IModeService,
@ILogService logService: ILogService
) {
super(modelUri, mimeType, file, fileService, modelService, modeService);
this.appendedMessage = '';
this.loadingFromFileInProgress = false;
// Use one rotating file to check for main file reset
this.appender = new OutputAppender(id, this.file.fsPath);
const rotatingFilePathDirectory = resources.dirname(this.file);
this.rotatingFilePath = resources.joinPath(rotatingFilePathDirectory, `${id}.1.log`);
this._register(fileService.watch(rotatingFilePathDirectory));
this._register(fileService.onDidFilesChange(e => {
if (e.contains(this.rotatingFilePath)) {
this.resettingDelayer.trigger(() => this.resetModel());
}
}));
this.resettingDelayer = new ThrottledDelayer<void>(50);
}
append(message: string): void {
// update end offset always as message is read
this.endOffset = this.endOffset + Buffer.from(message).byteLength;
if (this.loadingFromFileInProgress) {
this.appendedMessage += message;
} else {
this.write(message);
if (this.model) {
this.appendedMessage += message;
if (!this.modelUpdater.isScheduled()) {
this.modelUpdater.schedule();
}
}
}
}
clear(till?: number): void {
super.clear(till);
this.appendedMessage = '';
}
loadModel(): Promise<ITextModel> {
this.loadingFromFileInProgress = true;
if (this.modelUpdater.isScheduled()) {
this.modelUpdater.cancel();
}
this.appendedMessage = '';
return this.loadFile()
.then(content => {
if (this.endOffset !== this.startOffset + Buffer.from(content).byteLength) {
// Queue content is not written into the file
// Flush it and load file again
this.flush();
return this.loadFile();
}
return content;
})
.then(content => {
if (this.appendedMessage) {
this.write(this.appendedMessage);
this.appendedMessage = '';
}
this.loadingFromFileInProgress = false;
return this.createModel(content);
});
}
private resetModel(): Promise<void> {
this.startOffset = 0;
this.endOffset = 0;
if (this.model) {
return this.loadModel().then(() => undefined);
}
return Promise.resolve(undefined);
}
private loadFile(): Promise<string> {
return this.fileService.readFile(this.file, { position: this.startOffset })
.then(content => this.appendedMessage ? content.value + this.appendedMessage : content.value.toString());
}
protected updateModel(): void {
if (this.model && this.appendedMessage) {
this.appendToModel(this.appendedMessage);
this.appendedMessage = '';
}
}
private write(content: string): void {
this.appender.append(content);
}
private flush(): void {
this.appender.flush();
}
}
class DelegatedOutputChannelModel extends Disposable implements IOutputChannelModel {
private readonly _onDidAppendedContent: Emitter<void> = this._register(new Emitter<void>());
readonly onDidAppendedContent: Event<void> = this._onDidAppendedContent.event;
private readonly _onDispose: Emitter<void> = this._register(new Emitter<void>());
readonly onDispose: Event<void> = this._onDispose.event;
private readonly outputChannelModel: Promise<IOutputChannelModel>;
constructor(
id: string,
modelUri: URI,
mimeType: string,
outputDir: Promise<URI>,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILogService private readonly logService: ILogService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super();
this.outputChannelModel = this.createOutputChannelModel(id, modelUri, mimeType, outputDir);
}
private async createOutputChannelModel(id: string, modelUri: URI, mimeType: string, outputDirPromise: Promise<URI>): Promise<IOutputChannelModel> {
let outputChannelModel: IOutputChannelModel;
try {
const outputDir = await outputDirPromise;
const file = resources.joinPath(outputDir, `${id}.log`);
outputChannelModel = this.instantiationService.createInstance(OutputChannelBackedByFile, id, modelUri, mimeType, file);
} catch (e) {
// Do not crash if spdlog rotating logger cannot be loaded (workaround for https://github.com/microsoft/vscode/issues/47883)
this.logService.error(e);
this.telemetryService.publicLog2('output.channel.creation.error');
outputChannelModel = this.instantiationService.createInstance(BufferredOutputChannel, modelUri, mimeType);
}
this._register(outputChannelModel);
this._register(outputChannelModel.onDidAppendedContent(() => this._onDidAppendedContent.fire()));
this._register(outputChannelModel.onDispose(() => this._onDispose.fire()));
return outputChannelModel;
}
append(output: string): void {
this.outputChannelModel.then(outputChannelModel => outputChannelModel.append(output));
}
update(): void {
this.outputChannelModel.then(outputChannelModel => outputChannelModel.update());
}
loadModel(): Promise<ITextModel> {
return this.outputChannelModel.then(outputChannelModel => outputChannelModel.loadModel());
}
clear(till?: number): void {
this.outputChannelModel.then(outputChannelModel => outputChannelModel.clear(till));
}
}
export class OutputChannelModelService extends AsbtractOutputChannelModelService implements IOutputChannelModelService {
declare readonly _serviceBrand: undefined;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IFileService private readonly fileService: IFileService,
@INativeHostService private readonly nativeHostService: INativeHostService
) {
super(instantiationService);
}
createOutputChannelModel(id: string, modelUri: URI, mimeType: string, file?: URI): IOutputChannelModel {
return file ? super.createOutputChannelModel(id, modelUri, mimeType, file) :
this.instantiationService.createInstance(DelegatedOutputChannelModel, id, modelUri, mimeType, this.outputDir);
}
private _outputDir: Promise<URI> | null = null;
private get outputDir(): Promise<URI> {
if (!this._outputDir) {
const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.nativeHostService.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`));
this._outputDir = this.fileService.createFolder(outputDir).then(() => outputDir);
}
return this._outputDir;
}
}
registerSingleton(IOutputChannelModelService, OutputChannelModelService);

View File

@@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { createRotatingLogger } from 'vs/platform/log/node/spdlogService';
import { RotatingLogger } from 'spdlog';
import { ByteSize } from 'vs/platform/files/common/files';
export class OutputAppender {
private appender: RotatingLogger;
constructor(name: string, readonly file: string) {
this.appender = createRotatingLogger(name, file, 1024 * 1024 * 30, 1);
this.appender = createRotatingLogger(name, file, 30 * ByteSize.MB, 1);
this.appender.clearFormatters();
}
@@ -21,4 +22,4 @@ export class OutputAppender {
flush(): void {
this.appender.flush();
}
}
}

View File

@@ -29,7 +29,7 @@ import { EditorInput, IEditorPane } from 'vs/workbench/common/editor';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { GroupDirection, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, getSettingsTargetName, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, SettingsEditorOptions, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences';
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, getSettingsTargetName, IKeybindingsEditorOptions, IKeybindingsEditorPane, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, SettingsEditorOptions, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences';
import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel, DefaultRawSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
@@ -282,32 +282,38 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
}
openGlobalKeybindingSettings(textual: boolean): Promise<void> {
async openGlobalKeybindingSettings(textual: boolean, options?: IKeybindingsEditorOptions): Promise<void> {
type OpenKeybindingsClassification = {
textual: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
this.telemetryService.publicLog2<{ textual: boolean }, OpenKeybindingsClassification>('openKeybindings', { textual });
options = { pinned: true, revealIfOpened: true, ...options };
if (textual) {
const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + '\n[\n]';
const editableKeybindings = this.environmentService.keybindingsResource;
const openDefaultKeybindings = !!this.configurationService.getValue('workbench.settings.openDefaultKeybindings');
// Create as needed and open in editor
return this.createIfNotExists(editableKeybindings, emptyContents).then(() => {
if (openDefaultKeybindings) {
const activeEditorGroup = this.editorGroupService.activeGroup;
const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT);
return Promise.all([
this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }),
this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true, revealIfOpened: true } }, sideEditorGroup.id)
]).then(editors => undefined);
} else {
return this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true, revealIfOpened: true } }).then(() => undefined);
}
});
await this.createIfNotExists(editableKeybindings, emptyContents);
if (openDefaultKeybindings) {
const activeEditorGroup = this.editorGroupService.activeGroup;
const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT);
await Promise.all([
this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }),
this.editorService.openEditor({ resource: editableKeybindings, options }, sideEditorGroup.id)
]);
} else {
await this.editorService.openEditor({ resource: editableKeybindings, options });
}
} else {
const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), options)) as IKeybindingsEditorPane;
if (options.query) {
editor.search(options.query);
}
}
return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true, revealIfOpened: true }).then(() => undefined);
}
openDefaultKeybindingsFile(): Promise<IEditorPane | undefined> {
@@ -323,8 +329,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
}
const editor = await this.doOpenSettings(configurationTarget, resource, options, group);
if (editor && options?.editSetting) {
await this.editSetting(options?.editSetting, editor, resource);
if (editor && options?.revealSetting) {
await this.revealSetting(options.revealSetting.key, !!options.revealSetting.edit, editor, resource);
}
return editor;
}
@@ -578,11 +584,12 @@ export class PreferencesService extends Disposable implements IPreferencesServic
'editor.insertSpaces',
'editor.wordWrap',
'files.exclude',
'files.associations'
'files.associations',
'workbench.editor.enablePreview'
];
}
private async editSetting(settingKey: string, editor: IEditorPane, settingsResource: URI): Promise<void> {
private async revealSetting(settingKey: string, edit: boolean, editor: IEditorPane, settingsResource: URI): Promise<void> {
const codeEditor = editor ? getCodeEditor(editor.getControl()) : null;
if (!codeEditor) {
return;
@@ -591,16 +598,18 @@ export class PreferencesService extends Disposable implements IPreferencesServic
if (!settingsModel) {
return;
}
const position = await this.getPositionToEdit(settingKey, settingsModel, codeEditor);
const position = await this.getPositionToReveal(settingKey, edit, settingsModel, codeEditor);
if (position) {
codeEditor.setPosition(position);
codeEditor.revealPositionNearTop(position);
codeEditor.focus();
await this.commandService.executeCommand('editor.action.triggerSuggest');
if (edit) {
await this.commandService.executeCommand('editor.action.triggerSuggest');
}
}
}
private async getPositionToEdit(settingKey: string, settingsModel: IPreferencesEditorModel<ISetting>, codeEditor: ICodeEditor): Promise<IPosition | null> {
private async getPositionToReveal(settingKey: string, edit: boolean, settingsModel: IPreferencesEditorModel<ISetting>, codeEditor: ICodeEditor): Promise<IPosition | null> {
const model = codeEditor.getModel();
if (!model) {
return null;
@@ -613,7 +622,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
let position = null;
const type = schema ? schema.type : 'object' /* Override Identifier */;
let setting = settingsModel.getPreference(settingKey);
if (!setting) {
if (!setting && edit) {
const defaultValue = (type === 'object' || type === 'array') ? this.configurationService.inspect(settingKey).defaultValue : getDefaultValue(type);
if (defaultValue !== undefined) {
const key = settingsModel instanceof WorkspaceConfigurationEditorModel ? ['settings', settingKey] : [settingKey];
@@ -623,18 +632,22 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
if (setting) {
position = { lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 };
if (type === 'object' || type === 'array') {
codeEditor.setPosition(position);
await CoreEditingCommands.LineBreakInsert.runEditorCommand(null, codeEditor, null);
position = { lineNumber: position.lineNumber + 1, column: model.getLineMaxColumn(position.lineNumber + 1) };
const firstNonWhiteSpaceColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber);
if (firstNonWhiteSpaceColumn) {
// Line has some text. Insert another new line.
codeEditor.setPosition({ lineNumber: position.lineNumber, column: firstNonWhiteSpaceColumn });
if (edit) {
position = { lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 };
if (type === 'object' || type === 'array') {
codeEditor.setPosition(position);
await CoreEditingCommands.LineBreakInsert.runEditorCommand(null, codeEditor, null);
position = { lineNumber: position.lineNumber, column: model.getLineMaxColumn(position.lineNumber) };
position = { lineNumber: position.lineNumber + 1, column: model.getLineMaxColumn(position.lineNumber + 1) };
const firstNonWhiteSpaceColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber);
if (firstNonWhiteSpaceColumn) {
// Line has some text. Insert another new line.
codeEditor.setPosition({ lineNumber: position.lineNumber, column: firstNonWhiteSpaceColumn });
await CoreEditingCommands.LineBreakInsert.runEditorCommand(null, codeEditor, null);
position = { lineNumber: position.lineNumber, column: model.getLineMaxColumn(position.lineNumber) };
}
}
} else {
position = { lineNumber: setting.keyRange.startLineNumber, column: setting.keyRange.startColumn };
}
}

View File

@@ -21,6 +21,7 @@ import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingReso
export const KEYBINDING_ENTRY_TEMPLATE_ID = 'keybinding.entry.template';
const SOURCE_DEFAULT = localize('default', "Default");
const SOURCE_EXTENSION = localize('extension', "Extension");
const SOURCE_USER = localize('user', "User");
export interface KeybindingMatch {
@@ -92,9 +93,20 @@ export class KeybindingsEditorModel extends EditorModel {
fetch(searchValue: string, sortByPrecedence: boolean = false): IKeybindingItemEntry[] {
let keybindingItems = sortByPrecedence ? this._keybindingItemsSortedByPrecedence : this._keybindingItems;
const commandIdMatches = /@command:\s*(.+)/i.exec(searchValue);
if (commandIdMatches && commandIdMatches[1]) {
return keybindingItems.filter(k => k.command === commandIdMatches[1])
.map(keybindingItem => (<IKeybindingItemEntry>{ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem, templateId: KEYBINDING_ENTRY_TEMPLATE_ID }));
}
if (/@source:\s*(user|default)/i.test(searchValue)) {
keybindingItems = this.filterBySource(keybindingItems, searchValue);
searchValue = searchValue.replace(/@source:\s*(user|default)/i, '');
} else {
const keybindingMatches = /@keybinding:\s*((\".+\")|(\S+))/i.exec(searchValue);
if (keybindingMatches && (keybindingMatches[2] || keybindingMatches[3])) {
searchValue = keybindingMatches[2] || `"${keybindingMatches[3]}"`;
}
}
searchValue = searchValue.trim();
@@ -112,6 +124,9 @@ export class KeybindingsEditorModel extends EditorModel {
if (/@source:\s*user/i.test(searchValue)) {
return keybindingItems.filter(k => k.source === SOURCE_USER);
}
if (/@source:\s*extension/i.test(searchValue)) {
return keybindingItems.filter(k => k.source === SOURCE_EXTENSION);
}
return keybindingItems;
}
@@ -176,7 +191,7 @@ export class KeybindingsEditorModel extends EditorModel {
const commandsWithDefaultKeybindings = this.keybindingsService.getDefaultKeybindings().map(keybinding => keybinding.command);
for (const command of KeybindingResolver.getAllUnboundCommands(boundCommands)) {
const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1, null);
const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1, null, false);
this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, workbenchActionsRegistry, actionLabels));
}
this._keybindingItems = this._keybindingItemsSortedByPrecedence.slice(0).sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b));
@@ -221,7 +236,11 @@ export class KeybindingsEditorModel extends EditorModel {
commandLabel: KeybindingsEditorModel.getCommandLabel(menuCommand, editorActionLabel),
commandDefaultLabel: KeybindingsEditorModel.getCommandDefaultLabel(menuCommand, workbenchActionsRegistry),
when: keybindingItem.when ? keybindingItem.when.serialize() : '',
source: keybindingItem.isDefault ? SOURCE_DEFAULT : SOURCE_USER
source: (
keybindingItem.extensionId
? (keybindingItem.isBuiltinExtension ? SOURCE_DEFAULT : SOURCE_EXTENSION)
: (keybindingItem.isDefault ? SOURCE_DEFAULT : SOURCE_USER)
)
};
}

View File

@@ -18,6 +18,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { EditorOptions, IEditorPane } from 'vs/workbench/common/editor';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
export enum SettingValueType {
Null = 'null',
@@ -162,7 +163,10 @@ export interface ISettingsEditorOptions extends IEditorOptions {
target?: ConfigurationTarget;
folderUri?: URI;
query?: string;
editSetting?: string;
revealSetting?: {
key: string;
edit?: boolean;
};
}
/**
@@ -173,7 +177,10 @@ export class SettingsEditorOptions extends EditorOptions implements ISettingsEdi
target?: ConfigurationTarget;
folderUri?: URI;
query?: string;
editSetting?: string;
revealSetting?: {
key: string;
edit?: boolean;
};
static create(settings: ISettingsEditorOptions): SettingsEditorOptions {
const options = new SettingsEditorOptions();
@@ -182,7 +189,7 @@ export class SettingsEditorOptions extends EditorOptions implements ISettingsEdi
options.target = settings.target;
options.folderUri = settings.folderUri;
options.query = settings.query;
options.editSetting = settings.editSetting;
options.revealSetting = settings.revealSetting;
return options;
}
@@ -191,6 +198,10 @@ export class SettingsEditorOptions extends EditorOptions implements ISettingsEdi
export interface IKeybindingsEditorModel<T> extends IPreferencesEditorModel<T> {
}
export interface IKeybindingsEditorOptions extends IEditorOptions {
query?: string;
}
export const IPreferencesService = createDecorator<IPreferencesService>('preferencesService');
export interface IPreferencesService {
@@ -211,7 +222,7 @@ export interface IPreferencesService {
openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditorPane | undefined>;
openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditorPane | undefined>;
switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Promise<void>;
openGlobalKeybindingSettings(textual: boolean): Promise<void>;
openGlobalKeybindingSettings(textual: boolean, options?: IKeybindingsEditorOptions): Promise<void>;
openDefaultKeybindingsFile(): Promise<IEditorPane | undefined>;
getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise<URI | null>;
}
@@ -230,6 +241,29 @@ export function getSettingsTargetName(target: ConfigurationTarget, resource: URI
return '';
}
export interface IKeybindingsEditorPane extends IEditorPane {
readonly activeKeybindingEntry: IKeybindingItemEntry | null;
readonly onDefineWhenExpression: Event<IKeybindingItemEntry>;
readonly onLayout: Event<void>;
search(filter: string): void;
focusSearch(): void;
clearSearchResults(): void;
focusKeybindings(): void;
recordSearchKeys(): void;
toggleSortByPrecedence(): void;
selectKeybinding(keybindingEntry: IKeybindingItemEntry): void;
defineKeybinding(keybindingEntry: IKeybindingItemEntry, add: boolean): Promise<void>;
defineWhenExpression(keybindingEntry: IKeybindingItemEntry): void;
updateKeybinding(keybindingEntry: IKeybindingItemEntry, key: string, when: string | undefined): Promise<any>;
removeKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<any>;
resetKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<any>;
copyKeybinding(keybindingEntry: IKeybindingItemEntry): Promise<void>;
copyKeybindingCommand(keybindingEntry: IKeybindingItemEntry): Promise<void>;
showSimilarKeybindings(keybindingEntry: IKeybindingItemEntry): void;
}
export const FOLDER_SETTINGS_PATH = '.vscode/settings.json';
export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings';
export const USE_SPLIT_JSON_SETTING = 'workbench.settings.useSplitJSON';

View File

@@ -996,7 +996,9 @@ class SettingsContentBuilder {
`${displayEnum}: ${fixSettingLink(desc)}` :
displayEnum;
this._contentByLines.push(`${indent}// - ${line}`);
const lines = line.split(/\n/g);
lines[0] = ' - ' + lines[0];
this._contentByLines.push(...lines.map(l => `${indent}// ${l}`));
setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length });
});

View File

@@ -92,10 +92,12 @@ function valueValidatesAsType(value: any, type: string): boolean {
}
function getStringValidators(prop: IConfigurationPropertySchema) {
const uriRegex = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
let patternRegex: RegExp | undefined;
if (typeof prop.pattern === 'string') {
patternRegex = new RegExp(prop.pattern);
}
return [
{
enabled: prop.maxLength !== undefined,
@@ -116,7 +118,25 @@ function getStringValidators(prop: IConfigurationPropertySchema) {
enabled: prop.format === 'color-hex',
isValid: ((value: string) => Color.Format.CSS.parseHex(value)),
message: nls.localize('validations.colorFormat', "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.")
}
},
{
enabled: prop.format === 'uri' || prop.format === 'uri-reference',
isValid: ((value: string) => !!value.length),
message: nls.localize('validations.uriEmpty', "URI expected.")
},
{
enabled: prop.format === 'uri' || prop.format === 'uri-reference',
isValid: ((value: string) => uriRegex.test(value)),
message: nls.localize('validations.uriMissing', "URI is expected.")
},
{
enabled: prop.format === 'uri',
isValid: ((value: string) => {
const matches = value.match(uriRegex);
return !!(matches && matches[2]);
}),
message: nls.localize('validations.uriSchemeMissing', "URI with a scheme is expected.")
},
].filter(validation => validation.enabled);
}
@@ -249,5 +269,3 @@ function getArrayOfStringValidator(prop: IConfigurationPropertySchema): ((value:
return null;
}

View File

@@ -262,6 +262,29 @@ suite('KeybindingsEditorModel', () => {
assert.ok(actual);
});
test('filter by command prefix with different commands', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: true });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command: uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: true }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch(`@command:${command}`);
assert.equal(actual.length, 1);
assert.deepEqual(actual[0].keybindingItem.command, command);
});
test('filter by command prefix with same commands', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: true });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: true }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch(`@command:${command}`);
assert.equal(actual.length, 2);
assert.deepEqual(actual[0].keybindingItem.command, command);
assert.deepEqual(actual[1].keybindingItem.command, command);
});
test('filter by when context', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'whenContext1 && whenContext2', isDefault: false });
@@ -534,6 +557,18 @@ suite('KeybindingsEditorModel', () => {
assert.deepEqual(actual[0].keybindingMatches!.chordPart, {});
});
test('filter by keybinding prefix', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch('@keybinding:control+c').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches!.firstPart, { ctrlKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches!.chordPart, {});
});
test('filter matches with + separator in first and chord parts', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
@@ -546,6 +581,18 @@ suite('KeybindingsEditorModel', () => {
assert.deepEqual(actual[0].keybindingMatches!.chordPart, { keyCode: true, ctrlKey: true });
});
test('filter by keybinding prefix with chord', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch('@keybinding:"shift+meta+escape ctrl+c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches!.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches!.chordPart, { keyCode: true, ctrlKey: true });
});
test('filter exact matches with space #32993', async () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Space, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
@@ -658,7 +705,7 @@ suite('KeybindingsEditorModel', () => {
}
}
const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined;
return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null);
return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null, false);
}
function asResolvedKeybindingItems(keybindingEntries: IKeybindingItemEntry[], keepUnassigned: boolean = false): ResolvedKeybindingItem[] {

View File

@@ -373,4 +373,20 @@ suite('Preferences Validation', () => {
testInvalidTypeError([null], 'null', false);
testInvalidTypeError('null', 'null', false);
});
test('uri checks work', () => {
const tester = new Tester({ type: 'string', format: 'uri' });
tester.rejects('example.com');
tester.rejects('example.com/example');
tester.rejects('example/example.html');
tester.rejects('www.example.com');
tester.rejects('');
tester.rejects(' ');
tester.rejects('example');
tester.accepts('https:');
tester.accepts('https://');
tester.accepts('https://example.com');
tester.accepts('https://www.example.com');
});
});

View File

@@ -74,6 +74,13 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I
return this._environment;
}
whenExtensionsReady(): Promise<void> {
return this._withChannel(
channel => RemoteExtensionEnvironmentChannelClient.whenExtensionsReady(channel),
undefined
);
}
scanExtensions(skipExtensions: ExtensionIdentifier[] = []): Promise<IExtensionDescription[]> {
return this._withChannel(
(channel, connection) => RemoteExtensionEnvironmentChannelClient.scanExtensions(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI, skipExtensions),

Some files were not shown because too many files have changed in this diff Show More