mirror of
https://github.com/coder/code-server.git
synced 2026-05-08 21:37:27 +02:00
Update to VS Code 1.52.1
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'); }
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService {
|
||||
readonly sessionId: string;
|
||||
|
||||
readonly logFile: URI;
|
||||
readonly backupWorkspaceHome?: URI;
|
||||
|
||||
readonly extHostLogsPath: URI;
|
||||
readonly logExtensionHostCommunication?: boolean;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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()))
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -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);
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
lib/vscode/src/vs/workbench/services/issue/common/issue.ts
Normal file
15
lib/vscode/src/vs/workbench/services/issue/common/issue.ts
Normal 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>;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user