remove workbench/contrib/experiments

This commit is contained in:
isidor 2023-06-20 14:36:56 +02:00
parent b578213118
commit 6af591662a
13 changed files with 0 additions and 2130 deletions

View file

@ -74,10 +74,6 @@
"name": "vs/workbench/contrib/emmet",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/experiments",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/extensions",
"project": "vscode-workbench"

View file

@ -1,91 +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 { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification';
import { IExperimentService, IExperiment, ExperimentActionType, IExperimentActionPromptProperties, IExperimentActionPromptCommand, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService';
import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable } from 'vs/base/common/lifecycle';
import { language } from 'vs/base/common/platform';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { URI } from 'vs/base/common/uri';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { ViewContainerLocation } from 'vs/workbench/common/views';
export class ExperimentalPrompts extends Disposable implements IWorkbenchContribution {
constructor(
@IExperimentService private readonly experimentService: IExperimentService,
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
@INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService,
@ICommandService private readonly commandService: ICommandService
) {
super();
this._register(this.experimentService.onExperimentEnabled(e => {
if (e.action && e.action.type === ExperimentActionType.Prompt && e.state === ExperimentState.Run) {
this.showExperimentalPrompts(e);
}
}, this));
}
private showExperimentalPrompts(experiment: IExperiment): void {
if (!experiment || !experiment.enabled || !experiment.action || experiment.state !== ExperimentState.Run) {
return;
}
const actionProperties = (<IExperimentActionPromptProperties>experiment.action.properties);
const promptText = ExperimentalPrompts.getLocalizedText(actionProperties.promptText, language || '');
if (!actionProperties || !promptText) {
return;
}
if (!actionProperties.commands) {
actionProperties.commands = [];
}
const choices: IPromptChoice[] = actionProperties.commands.map((command: IExperimentActionPromptCommand) => {
const commandText = ExperimentalPrompts.getLocalizedText(command.text, language || '');
return {
label: commandText,
run: () => {
if (command.externalLink) {
this.openerService.open(URI.parse(command.externalLink));
} else if (command.curatedExtensionsKey && Array.isArray(command.curatedExtensionsList)) {
this.paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet?.search('curated:' + command.curatedExtensionsKey);
});
} else if (command.codeCommand) {
this.commandService.executeCommand(command.codeCommand.id, ...command.codeCommand.arguments);
}
this.experimentService.markAsCompleted(experiment.id);
}
};
});
this.notificationService.prompt(Severity.Info, promptText, choices, {
onCancel: () => {
this.experimentService.markAsCompleted(experiment.id);
}
});
}
static getLocalizedText(text: string | { [key: string]: string }, displayLanguage: string): string {
if (typeof text === 'string') {
return text;
}
const msgInEnglish = text['en'] || text['en-us'];
displayLanguage = displayLanguage.toLowerCase();
if (!text[displayLanguage] && displayLanguage.indexOf('-') === 2) {
displayLanguage = displayLanguage.substr(0, 2);
}
return text[displayLanguage] || msgInEnglish;
}
}

View file

@ -1,35 +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 { localize } from 'vs/nls';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
registerSingleton(IExperimentService, ExperimentService, InstantiationType.Delayed);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExperimentalPrompts, LifecyclePhase.Eventually);
const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
// Configuration
registry.registerConfiguration({
...workbenchConfigurationNodeBase,
'properties': {
'workbench.enableExperiments': {
'type': 'boolean',
'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),
'default': true,
'scope': ConfigurationScope.APPLICATION,
'restricted': true,
'tags': ['usesOnlineServices']
}
}
});

View file

@ -1,593 +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 { distinct } from 'vs/base/common/arrays';
import { RunOnceWorker } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { match } from 'vs/base/common/glob';
import { Disposable } from 'vs/base/common/lifecycle';
import { equals } from 'vs/base/common/objects';
import { language, OperatingSystem, OS } from 'vs/base/common/platform';
import { isDefined } from 'vs/base/common/types';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProductService } from 'vs/platform/product/common/productService';
import { asJson, IRequestService } from 'vs/platform/request/common/request';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
export const enum ExperimentState {
Evaluating,
NoRun,
Run,
Complete
}
export interface IExperimentAction {
type: ExperimentActionType;
properties: any;
}
export enum ExperimentActionType {
Custom = 'Custom',
Prompt = 'Prompt',
AddToRecommendations = 'AddToRecommendations',
ExtensionSearchResults = 'ExtensionSearchResults'
}
export type LocalizedPromptText = { [locale: string]: string };
export interface IExperimentActionPromptProperties {
promptText: string | LocalizedPromptText;
commands: IExperimentActionPromptCommand[];
}
export interface IExperimentActionPromptCommand {
text: string | { [key: string]: string };
externalLink?: string;
curatedExtensionsKey?: string;
curatedExtensionsList?: string[];
codeCommand?: {
id: string;
arguments: unknown[];
};
}
export interface IExperiment {
id: string;
enabled: boolean;
raw: IRawExperiment | undefined;
state: ExperimentState;
action?: IExperimentAction;
}
export interface IExperimentService {
readonly _serviceBrand: undefined;
getExperimentById(id: string): Promise<IExperiment>;
getExperimentsByType(type: ExperimentActionType): Promise<IExperiment[]>;
markAsCompleted(experimentId: string): void;
onExperimentEnabled: Event<IExperiment>;
}
export const IExperimentService = createDecorator<IExperimentService>('experimentService');
interface IExperimentStorageState {
enabled: boolean;
state: ExperimentState;
editCount?: number;
lastEditedDate?: string;
}
/**
* Current version of the experiment schema in this VS Code build. This *must*
* be incremented when adding a condition, otherwise experiments might activate
* on older versions of VS Code where not intended.
*/
export const currentSchemaVersion = 5;
interface IRawExperiment {
id: string;
schemaVersion: number;
enabled?: boolean;
condition?: {
insidersOnly?: boolean;
newUser?: boolean;
displayLanguage?: string;
// Evaluates to true iff all the given user settings are deeply equal
userSetting?: { [key: string]: unknown };
// Start the experiment if the number of activation events have happened over the last week:
activationEvent?: {
event: string | string[];
uniqueDays?: number;
minEvents: number;
};
os: OperatingSystem[];
installedExtensions?: {
excludes?: string[];
includes?: string[];
};
fileEdits?: {
filePathPattern?: string;
workspaceIncludes?: string[];
workspaceExcludes?: string[];
minEditCount: number;
};
experimentsPreviouslyRun?: {
excludes?: string[];
includes?: string[];
};
userProbability?: number;
};
action?: IExperimentAction;
action2?: IExperimentAction;
}
interface IActivationEventRecord {
count: number[];
mostRecentBucket: number;
}
const experimentEventStorageKey = (event: string) => 'experimentEventRecord-' + event.replace(/[^0-9a-z]/ig, '-');
/**
* Updates the activation record to shift off days outside the window
* we're interested in.
*/
export const getCurrentActivationRecord = (previous?: IActivationEventRecord, dayWindow = 7): IActivationEventRecord => {
const oneDay = 1000 * 60 * 60 * 24;
const now = Date.now();
if (!previous) {
return { count: new Array(dayWindow).fill(0), mostRecentBucket: now };
}
// get the number of days, up to dayWindow, that passed since the last bucket update
const shift = Math.min(dayWindow, Math.floor((now - previous.mostRecentBucket) / oneDay));
if (!shift) {
return previous;
}
return {
count: new Array(shift).fill(0).concat(previous.count.slice(0, -shift)),
mostRecentBucket: previous.mostRecentBucket + shift * oneDay,
};
};
export class ExperimentService extends Disposable implements IExperimentService {
declare readonly _serviceBrand: undefined;
private _experiments: IExperiment[] = [];
private _loadExperimentsPromise: Promise<void>;
private _curatedMapping = Object.create(null);
private readonly _onExperimentEnabled = this._register(new Emitter<IExperiment>());
onExperimentEnabled: Event<IExperiment> = this._onExperimentEnabled.event;
constructor(
@IStorageService private readonly storageService: IStorageService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@ITextFileService private readonly textFileService: ITextFileService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IRequestService private readonly requestService: IRequestService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IProductService private readonly productService: IProductService,
@IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService,
@IExtensionService private readonly extensionService: IExtensionService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
) {
super();
this._loadExperimentsPromise = Promise.resolve(this.lifecycleService.when(LifecyclePhase.Eventually)).then(() =>
this.loadExperiments());
}
public getExperimentById(id: string): Promise<IExperiment> {
return this._loadExperimentsPromise.then(() => {
return this._experiments.filter(x => x.id === id)[0];
});
}
public getExperimentsByType(type: ExperimentActionType): Promise<IExperiment[]> {
return this._loadExperimentsPromise.then(() => {
if (type === ExperimentActionType.Custom) {
return this._experiments.filter(x => x.enabled && (!x.action || x.action.type === type));
}
return this._experiments.filter(x => x.enabled && x.action && x.action.type === type);
});
}
public markAsCompleted(experimentId: string): void {
const storageKey = 'experiments.' + experimentId;
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {});
experimentState.state = ExperimentState.Complete;
this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
protected async getExperiments(): Promise<IRawExperiment[] | null> {
if (this.environmentService.enableSmokeTestDriver || this.environmentService.extensionTestsLocationURI) {
return []; // TODO@sbatten add CLI argument (https://github.com/microsoft/vscode-internalbacklog/issues/2855)
}
const experimentsUrl = this.productService.experimentsUrl;
if (!experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) {
return [];
}
try {
const context = await this.requestService.request({ type: 'GET', url: experimentsUrl }, CancellationToken.None);
if (context.res.statusCode !== 200) {
return null;
}
const result = await asJson<{ experiments?: IRawExperiment }>(context);
return result && Array.isArray(result.experiments) ? result.experiments : [];
} catch (_e) {
// Bad request or invalid JSON
return null;
}
}
private loadExperiments(): Promise<any> {
return this.getExperiments().then(rawExperiments => {
// Offline mode
if (!rawExperiments) {
const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.APPLICATION), []);
if (Array.isArray(allExperimentIdsFromStorage)) {
allExperimentIdsFromStorage.forEach(experimentId => {
const storageKey = 'experiments.' + experimentId;
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), null);
if (experimentState) {
this._experiments.push({
id: experimentId,
raw: undefined,
enabled: experimentState.enabled,
state: experimentState.state
});
}
});
}
return Promise.resolve(null);
}
// Don't look at experiments with newer schema versions. We can't
// understand them, trying to process them might even cause errors.
rawExperiments = rawExperiments.filter(e => (e.schemaVersion || 0) <= currentSchemaVersion);
// Clear disbaled/deleted experiments from storage
const allExperimentIdsFromStorage = safeParse(this.storageService.get('allExperiments', StorageScope.APPLICATION), []);
const enabledExperiments = rawExperiments.filter(experiment => !!experiment.enabled).map(experiment => experiment.id.toLowerCase());
if (Array.isArray(allExperimentIdsFromStorage)) {
allExperimentIdsFromStorage.forEach(experiment => {
if (enabledExperiments.indexOf(experiment) === -1) {
this.storageService.remove(`experiments.${experiment}`, StorageScope.APPLICATION);
}
});
}
if (enabledExperiments.length) {
this.storageService.store('allExperiments', JSON.stringify(enabledExperiments), StorageScope.APPLICATION, StorageTarget.MACHINE);
} else {
this.storageService.remove('allExperiments', StorageScope.APPLICATION);
}
const activationEvents = new Set(rawExperiments.map(exp => exp.condition?.activationEvent?.event)
.filter(isDefined).flatMap(evt => typeof evt === 'string' ? [evt] : []));
if (activationEvents.size) {
this._register(this.extensionService.onWillActivateByEvent(evt => {
if (activationEvents.has(evt.event)) {
this.recordActivatedEvent(evt.event);
}
}));
}
const promises = rawExperiments.map(experiment => this.evaluateExperiment(experiment));
return Promise.all(promises).then(() => {
type ExperimentsClassification = {
owner: 'sbatten';
comment: 'Information about the experiments in this session';
experiments: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The list of experiments in this session' };
};
this.telemetryService.publicLog2<{ experiments: string[] }, ExperimentsClassification>('experiments', { experiments: this._experiments.map(e => e.id) });
});
});
}
private evaluateExperiment(experiment: IRawExperiment) {
const processedExperiment: IExperiment = {
id: experiment.id,
raw: experiment,
enabled: !!experiment.enabled,
state: !!experiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun
};
const action = experiment.action2 || experiment.action;
if (action) {
processedExperiment.action = {
type: ExperimentActionType[action.type] || ExperimentActionType.Custom,
properties: action.properties
};
if (processedExperiment.action.type === ExperimentActionType.Prompt) {
((<IExperimentActionPromptProperties>processedExperiment.action.properties).commands || []).forEach(x => {
if (x.curatedExtensionsKey && Array.isArray(x.curatedExtensionsList)) {
this._curatedMapping[experiment.id] = x;
}
});
}
if (!processedExperiment.action.properties) {
processedExperiment.action.properties = {};
}
}
this._experiments = this._experiments.filter(e => e.id !== processedExperiment.id);
this._experiments.push(processedExperiment);
if (!processedExperiment.enabled) {
return Promise.resolve(null);
}
const storageKey = 'experiments.' + experiment.id;
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {});
if (!experimentState.hasOwnProperty('enabled')) {
experimentState.enabled = processedExperiment.enabled;
}
if (!experimentState.hasOwnProperty('state')) {
experimentState.state = processedExperiment.enabled ? ExperimentState.Evaluating : ExperimentState.NoRun;
} else {
processedExperiment.state = experimentState.state;
}
return this.shouldRunExperiment(experiment, processedExperiment).then((state: ExperimentState) => {
experimentState.state = processedExperiment.state = state;
this.storageService.store(storageKey, JSON.stringify(experimentState), StorageScope.APPLICATION, StorageTarget.MACHINE);
if (state === ExperimentState.Run) {
this.fireRunExperiment(processedExperiment);
}
return Promise.resolve(null);
});
}
private fireRunExperiment(experiment: IExperiment) {
this._onExperimentEnabled.fire(experiment);
const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.APPLICATION), []);
if (runExperimentIdsFromStorage.indexOf(experiment.id) === -1) {
runExperimentIdsFromStorage.push(experiment.id);
}
// Ensure we dont store duplicates
const distinctExperiments = distinct(runExperimentIdsFromStorage);
if (runExperimentIdsFromStorage.length !== distinctExperiments.length) {
this.storageService.store('currentOrPreviouslyRunExperiments', JSON.stringify(distinctExperiments), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
}
private checkExperimentDependencies(experiment: IRawExperiment): boolean {
const experimentsPreviouslyRun = experiment.condition?.experimentsPreviouslyRun;
if (experimentsPreviouslyRun) {
const runExperimentIdsFromStorage: string[] = safeParse(this.storageService.get('currentOrPreviouslyRunExperiments', StorageScope.APPLICATION), []);
let includeCheck = true;
let excludeCheck = true;
const includes = experimentsPreviouslyRun.includes;
if (Array.isArray(includes)) {
includeCheck = runExperimentIdsFromStorage.some(x => includes.indexOf(x) > -1);
}
const excludes = experimentsPreviouslyRun.excludes;
if (includeCheck && Array.isArray(excludes)) {
excludeCheck = !runExperimentIdsFromStorage.some(x => excludes.indexOf(x) > -1);
}
if (!includeCheck || !excludeCheck) {
return false;
}
}
return true;
}
private recordActivatedEvent(event: string) {
const key = experimentEventStorageKey(event);
const record = getCurrentActivationRecord(safeParse(this.storageService.get(key, StorageScope.APPLICATION), undefined));
record.count[0]++;
this.storageService.store(key, JSON.stringify(record), StorageScope.APPLICATION, StorageTarget.MACHINE);
this._experiments
.filter(e => {
const lookingFor = e.raw?.condition?.activationEvent?.event;
if (e.state !== ExperimentState.Evaluating || !lookingFor) {
return false;
}
return typeof lookingFor === 'string' ? lookingFor === event : lookingFor?.includes(event);
})
.forEach(e => this.evaluateExperiment(e.raw!));
}
private checkActivationEventFrequency(experiment: IRawExperiment) {
const setting = experiment.condition?.activationEvent;
if (!setting) {
return true;
}
let total = 0;
let uniqueDays = 0;
const events = typeof setting.event === 'string' ? [setting.event] : setting.event;
for (const event of events) {
const { count } = getCurrentActivationRecord(safeParse(this.storageService.get(experimentEventStorageKey(event), StorageScope.APPLICATION), undefined));
for (const entry of count) {
if (entry > 0) {
uniqueDays++;
total += entry;
}
}
}
return total >= setting.minEvents && (!setting.uniqueDays || uniqueDays >= setting.uniqueDays);
}
private shouldRunExperiment(experiment: IRawExperiment, processedExperiment: IExperiment): Promise<ExperimentState> {
if (processedExperiment.state !== ExperimentState.Evaluating) {
return Promise.resolve(processedExperiment.state);
}
if (!experiment.enabled) {
return Promise.resolve(ExperimentState.NoRun);
}
const condition = experiment.condition;
if (!condition) {
return Promise.resolve(ExperimentState.Run);
}
if (experiment.condition?.os && !experiment.condition.os.includes(OS)) {
return Promise.resolve(ExperimentState.NoRun);
}
if (!this.checkExperimentDependencies(experiment)) {
return Promise.resolve(ExperimentState.NoRun);
}
for (const [key, value] of Object.entries(experiment.condition?.userSetting || {})) {
if (!equals(this.configurationService.getValue(key), value)) {
return Promise.resolve(ExperimentState.NoRun);
}
}
if (!this.checkActivationEventFrequency(experiment)) {
return Promise.resolve(ExperimentState.Evaluating);
}
if (this.productService.quality === 'stable' && condition.insidersOnly === true) {
return Promise.resolve(ExperimentState.NoRun);
}
const isNewUser = !this.storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION);
if ((condition.newUser === true && !isNewUser)
|| (condition.newUser === false && isNewUser)) {
return Promise.resolve(ExperimentState.NoRun);
}
if (typeof condition.displayLanguage === 'string') {
let localeToCheck = condition.displayLanguage.toLowerCase();
let displayLanguage = language!.toLowerCase();
if (localeToCheck !== displayLanguage) {
const a = displayLanguage.indexOf('-');
const b = localeToCheck.indexOf('-');
if (a > -1) {
displayLanguage = displayLanguage.substr(0, a);
}
if (b > -1) {
localeToCheck = localeToCheck.substr(0, b);
}
if (displayLanguage !== localeToCheck) {
return Promise.resolve(ExperimentState.NoRun);
}
}
}
if (!condition.userProbability) {
condition.userProbability = 1;
}
let extensionsCheckPromise = Promise.resolve(true);
const installedExtensions = condition.installedExtensions;
if (installedExtensions) {
extensionsCheckPromise = this.extensionManagementService.getInstalled(ExtensionType.User).then(locals => {
let includesCheck = true;
let excludesCheck = true;
const localExtensions = locals.map(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}`);
if (Array.isArray(installedExtensions.includes) && installedExtensions.includes.length) {
const extensionIncludes = installedExtensions.includes.map(e => e.toLowerCase());
includesCheck = localExtensions.some(e => extensionIncludes.indexOf(e) > -1);
}
if (Array.isArray(installedExtensions.excludes) && installedExtensions.excludes.length) {
const extensionExcludes = installedExtensions.excludes.map(e => e.toLowerCase());
excludesCheck = !localExtensions.some(e => extensionExcludes.indexOf(e) > -1);
}
return includesCheck && excludesCheck;
});
}
const storageKey = 'experiments.' + experiment.id;
const experimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {});
return extensionsCheckPromise.then(success => {
const fileEdits = condition.fileEdits;
if (!success || !fileEdits || typeof fileEdits.minEditCount !== 'number') {
const runExperiment = success && typeof condition.userProbability === 'number' && Math.random() < condition.userProbability;
return runExperiment ? ExperimentState.Run : ExperimentState.NoRun;
}
experimentState.editCount = experimentState.editCount || 0;
if (experimentState.editCount >= fileEdits.minEditCount) {
return ExperimentState.Run;
}
// Process model-save event every 250ms to reduce load
const onModelsSavedWorker = this._register(new RunOnceWorker<ITextFileEditorModel>(models => {
const date = new Date().toDateString();
const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.APPLICATION), {});
if (latestExperimentState.state !== ExperimentState.Evaluating) {
onSaveHandler.dispose();
onModelsSavedWorker.dispose();
return;
}
models.forEach(async model => {
if (latestExperimentState.state !== ExperimentState.Evaluating
|| date === latestExperimentState.lastEditedDate
|| (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount)
) {
return;
}
let filePathCheck = true;
let workspaceCheck = true;
if (typeof fileEdits.filePathPattern === 'string') {
filePathCheck = match(fileEdits.filePathPattern, model.resource.fsPath);
}
if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) {
const tags = await this.workspaceTagsService.getTags();
workspaceCheck = !!tags && fileEdits.workspaceIncludes.some(x => !!tags[x]);
}
if (workspaceCheck && Array.isArray(fileEdits.workspaceExcludes) && fileEdits.workspaceExcludes.length) {
const tags = await this.workspaceTagsService.getTags();
workspaceCheck = !!tags && !fileEdits.workspaceExcludes.some(x => !!tags[x]);
}
if (filePathCheck && workspaceCheck) {
latestExperimentState.editCount = (latestExperimentState.editCount || 0) + 1;
latestExperimentState.lastEditedDate = date;
this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
});
if (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) {
processedExperiment.state = latestExperimentState.state = (typeof condition.userProbability === 'number' && Math.random() < condition.userProbability && this.checkExperimentDependencies(experiment)) ? ExperimentState.Run : ExperimentState.NoRun;
this.storageService.store(storageKey, JSON.stringify(latestExperimentState), StorageScope.APPLICATION, StorageTarget.MACHINE);
if (latestExperimentState.state === ExperimentState.Run && processedExperiment.action && ExperimentActionType[processedExperiment.action.type] === ExperimentActionType.Prompt) {
this.fireRunExperiment(processedExperiment);
}
}
}, 250));
const onSaveHandler = this._register(this.textFileService.files.onDidSave(e => onModelsSavedWorker.work(e.model)));
return ExperimentState.Evaluating;
});
}
}
function safeParse(text: string | undefined, defaultObject: any) {
try {
return text ? JSON.parse(text) || defaultObject : defaultObject;
} catch (e) {
return defaultObject;
}
}

View file

@ -1,216 +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 assert from 'assert';
import { Emitter } from 'vs/base/common/event';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { INotificationService, IPromptChoice, IPromptOptions, Severity } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt';
import { ExperimentActionType, ExperimentState, IExperiment, IExperimentActionPromptProperties, IExperimentService, LocalizedPromptText } from 'vs/workbench/contrib/experiments/common/experimentService';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-sandbox/experimentService.test';
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
import { TestCommandService } from 'vs/editor/test/browser/editorTestServices';
import { ICommandService } from 'vs/platform/commands/common/commands';
suite('Experimental Prompts', () => {
let instantiationService: TestInstantiationService;
let experimentService: TestExperimentService;
let experimentalPrompt: ExperimentalPrompts;
let commandService: TestCommandService;
let onExperimentEnabledEvent: Emitter<IExperiment>;
let storageData: { [key: string]: any } = {};
const promptText = 'Hello there! Can you see this?';
const experiment: IExperiment =
{
id: 'experiment1',
enabled: true,
raw: undefined,
state: ExperimentState.Run,
action: {
type: ExperimentActionType.Prompt,
properties: {
promptText,
commands: [
{
text: 'Yes',
},
{
text: 'No'
}
]
}
}
};
suiteSetup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(ILifecycleService, new TestLifecycleService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
onExperimentEnabledEvent = new Emitter<IExperiment>();
});
setup(() => {
storageData = {};
instantiationService.stub(IStorageService, <Partial<IStorageService>>{
get: (a: string, b: StorageScope, c?: string) => a === 'experiments.experiment1' ? JSON.stringify(storageData) : c,
store: (a, b, c, d) => {
if (a === 'experiments.experiment1') {
storageData = JSON.parse(b + '');
}
}
});
instantiationService.stub(INotificationService, new TestNotificationService());
experimentService = instantiationService.createInstance(TestExperimentService);
experimentService.onExperimentEnabled = onExperimentEnabledEvent.event;
instantiationService.stub(IExperimentService, experimentService);
commandService = instantiationService.createInstance(TestCommandService);
instantiationService.stub(ICommandService, commandService);
});
teardown(() => {
if (experimentService) {
experimentService.dispose();
}
if (experimentalPrompt) {
experimentalPrompt.dispose();
}
});
test('Show experimental prompt if experiment should be run. Choosing negative option should mark experiment as complete', () => {
storageData = {
enabled: true,
state: ExperimentState.Run
};
instantiationService.stub(INotificationService, {
prompt: (a: Severity, b: string, c: IPromptChoice[]) => {
assert.strictEqual(b, promptText);
assert.strictEqual(c.length, 2);
c[1].run();
return undefined!;
}
});
experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts);
onExperimentEnabledEvent.fire(experiment);
return Promise.resolve(null).then(result => {
assert.strictEqual(storageData['state'], ExperimentState.Complete);
});
});
test('runs experiment command', () => {
storageData = {
enabled: true,
state: ExperimentState.Run
};
const stub = instantiationService.stub(ICommandService, 'executeCommand', () => undefined);
instantiationService.stub(INotificationService, {
prompt: (a: Severity, b: string, c: IPromptChoice[], options: IPromptOptions) => {
c[0].run();
return undefined!;
}
});
experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts);
onExperimentEnabledEvent.fire({
...experiment,
action: {
type: ExperimentActionType.Prompt,
properties: {
promptText,
commands: [
{
text: 'Yes',
codeCommand: { id: 'greet', arguments: ['world'] }
}
]
}
}
});
return Promise.resolve(null).then(result => {
assert.deepStrictEqual(stub.args[0], ['greet', 'world']);
assert.strictEqual(storageData['state'], ExperimentState.Complete);
});
});
test('Show experimental prompt if experiment should be run. Cancelling should mark experiment as complete', () => {
storageData = {
enabled: true,
state: ExperimentState.Run
};
instantiationService.stub(INotificationService, {
prompt: (a: Severity, b: string, c: IPromptChoice[], options: IPromptOptions) => {
assert.strictEqual(b, promptText);
assert.strictEqual(c.length, 2);
options.onCancel!();
return undefined!;
}
});
experimentalPrompt = instantiationService.createInstance(ExperimentalPrompts);
onExperimentEnabledEvent.fire(experiment);
return Promise.resolve(null).then(result => {
assert.strictEqual(storageData['state'], ExperimentState.Complete);
});
});
test('Test getPromptText', () => {
const simpleTextCase: IExperimentActionPromptProperties = {
promptText: 'My simple prompt',
commands: []
};
const multipleLocaleCase: IExperimentActionPromptProperties = {
promptText: {
en: 'My simple prompt for en',
de: 'My simple prompt for de',
'en-au': 'My simple prompt for Austrailian English',
'en-us': 'My simple prompt for US English'
},
commands: []
};
const englishUSTextCase: IExperimentActionPromptProperties = {
promptText: {
'en-us': 'My simple prompt for en'
},
commands: []
};
const noEnglishTextCase: IExperimentActionPromptProperties = {
promptText: {
'de-de': 'My simple prompt for German'
},
commands: []
};
assert.strictEqual(ExperimentalPrompts.getLocalizedText(simpleTextCase.promptText, 'any-language'), simpleTextCase.promptText);
const multipleLocalePromptText = multipleLocaleCase.promptText as LocalizedPromptText;
assert.strictEqual(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'en'), multipleLocalePromptText['en']);
assert.strictEqual(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'de'), multipleLocalePromptText['de']);
assert.strictEqual(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'en-au'), multipleLocalePromptText['en-au']);
assert.strictEqual(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'en-gb'), multipleLocalePromptText['en']);
assert.strictEqual(ExperimentalPrompts.getLocalizedText(multipleLocaleCase.promptText, 'fr'), multipleLocalePromptText['en']);
assert.strictEqual(ExperimentalPrompts.getLocalizedText(englishUSTextCase.promptText, 'fr'), (englishUSTextCase.promptText as LocalizedPromptText)['en-us']);
assert.strictEqual(!!ExperimentalPrompts.getLocalizedText(noEnglishTextCase.promptText, 'fr'), false);
});
});

View file

@ -1,49 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService';
import { isString } from 'vs/base/common/types';
import { EXTENSION_IDENTIFIER_REGEX } from 'vs/platform/extensionManagement/common/extensionManagement';
export class ExperimentalRecommendations extends ExtensionRecommendations {
private _recommendations: ExtensionRecommendation[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
@IExperimentService private readonly experimentService: IExperimentService,
) {
super();
}
/**
* Fetch extensions used by others on the same workspace as recommendations
*/
protected async doActivate(): Promise<void> {
const experiments = await this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations);
for (const { action, state } of experiments) {
if (state === ExperimentState.Run && isNonEmptyArray(action?.properties?.recommendations) && action?.properties?.recommendationReason) {
for (const extensionId of action.properties.recommendations) {
try {
if (isString(extensionId) && EXTENSION_IDENTIFIER_REGEX.test(extensionId)) {
this._recommendations.push({
extensionId: extensionId.toLowerCase(),
reason: {
reasonId: ExtensionRecommendationReason.Experimental,
reasonText: action.properties.recommendationReason
}
});
}
} catch (error) {/* ignore */ }
}
}
}
}
}

View file

@ -13,7 +13,6 @@ import { Emitter, Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { LifecyclePhase, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { ExeBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/exeBasedRecommendations';
import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/browser/experimentalRecommendations';
import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations';
import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations';
import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations';
@ -42,7 +41,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
// Recommendations
private readonly fileBasedRecommendations: FileBasedRecommendations;
private readonly workspaceRecommendations: WorkspaceRecommendations;
private readonly experimentalRecommendations: ExperimentalRecommendations;
private readonly configBasedRecommendations: ConfigBasedRecommendations;
private readonly exeBasedRecommendations: ExeBasedRecommendations;
private readonly keymapRecommendations: KeymapRecommendations;
@ -71,7 +69,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations);
@ -101,7 +98,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this.workspaceRecommendations.activate(),
this.configBasedRecommendations.activate(),
this.fileBasedRecommendations.activate(),
this.experimentalRecommendations.activate(),
this.keymapRecommendations.activate(),
this.languageRecommendations.activate(),
this.webRecommendations.activate(),
@ -138,7 +134,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
const allRecommendations = [
...this.configBasedRecommendations.recommendations,
...this.exeBasedRecommendations.recommendations,
...this.experimentalRecommendations.recommendations,
...this.fileBasedRecommendations.recommendations,
...this.workspaceRecommendations.recommendations,
...this.keymapRecommendations.recommendations,
@ -170,7 +165,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
const recommendations = [
...this.configBasedRecommendations.otherRecommendations,
...this.exeBasedRecommendations.otherRecommendations,
...this.experimentalRecommendations.recommendations,
...this.webRecommendations.recommendations
];

View file

@ -36,8 +36,6 @@ import { IModelService } from 'vs/editor/common/services/model';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { INotificationService, Severity, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
import { NativeURLService } from 'vs/platform/url/common/urlService';
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-sandbox/experimentService.test';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services';
@ -198,7 +196,6 @@ suite('ExtensionRecommendationsService Test', () => {
let prompted: boolean;
const promptedEmitter = new Emitter<void>();
let onModelAddedEvent: Emitter<ITextModel>;
let experimentService: TestExperimentService;
suiteSetup(() => {
instantiationService = new TestInstantiationService();
@ -283,18 +280,12 @@ suite('ExtensionRecommendationsService Test', () => {
},
});
experimentService = instantiationService.createInstance(TestExperimentService);
instantiationService.stub(IExperimentService, experimentService);
instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService));
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(TestExtensionTipsService));
onModelAddedEvent = new Emitter<ITextModel>();
});
suiteTeardown(() => {
experimentService?.dispose();
});
setup(() => {
instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{});
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []);

View file

@ -42,8 +42,6 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { Schemas } from 'vs/base/common/network';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-sandbox/experimentService.test';
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { TestEnvironmentService, TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
import { DisposableStore } from 'vs/base/common/lifecycle';
@ -129,7 +127,6 @@ function setupTest() {
instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter<IFormatterChangeEvent>().event });
instantiationService.stub(ILifecycleService, new TestLifecycleService());
instantiationService.stub(IExperimentService, instantiationService.createInstance(TestExperimentService));
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(TestExtensionTipsService));
instantiationService.stub(IExtensionRecommendationsService, {});
instantiationService.stub(IURLService, NativeURLService);

View file

@ -33,7 +33,6 @@ import { NativeURLService } from 'vs/platform/url/common/urlService';
import { URI } from 'vs/base/common/uri';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { SinonStub } from 'sinon';
import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService';
import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions';
@ -92,7 +91,6 @@ suite('ExtensionsViews Tests', () => {
instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService);
instantiationService.stub(ISharedProcessService, TestSharedProcessService);
instantiationService.stub(IExperimentService, ExperimentService);
instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
onInstallExtension: installEvent.event,
@ -174,7 +172,6 @@ suite('ExtensionsViews Tests', () => {
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(galleryEnabledLanguage));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', galleryEnabledLanguage);
instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', [galleryEnabledLanguage]);
instantiationService.stubPromise(IExperimentService, 'getExperimentsByType', []);
instantiationService.stub(IViewDescriptorService, {
getViewLocationById(): ViewContainerLocation {

View file

@ -42,8 +42,6 @@ import { TestContextService } from 'vs/workbench/test/common/workbenchTestServic
import { IProductService } from 'vs/platform/product/common/productService';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-sandbox/experimentService.test';
import { Schemas } from 'vs/base/common/network';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
@ -116,7 +114,6 @@ suite('ExtensionsWorkbenchServiceTest', () => {
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
instantiationService.stub(ILifecycleService, new TestLifecycleService());
instantiationService.stub(IExperimentService, instantiationService.createInstance(TestExperimentService));
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(TestExtensionTipsService));
instantiationService.stub(IExtensionRecommendationsService, {});

View file

@ -325,9 +325,6 @@ import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contrib
// Language Status
import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution';
// Experiments
import 'vs/workbench/contrib/experiments/browser/experiments.contribution';
// Send a Smile
import 'vs/workbench/contrib/feedback/browser/feedback.contribution';