Skip to content

Commit

Permalink
doneOn => completionEvents
Browse files Browse the repository at this point in the history
implement onLink, onEvent, onCommand, extensionInstalled, stepSelected
ref #122570
  • Loading branch information
Jackson Kearl committed May 13, 2021
1 parent 1f2077f commit 4c63cdf
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 60 deletions.
6 changes: 3 additions & 3 deletions src/vs/platform/extensions/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ export interface IWalkthroughStep {
readonly id: string;
readonly title: string;
readonly description: string | undefined;
readonly media:
| { path: string | { dark: string, light: string, hc: string }, altText: string }
| { path: string, },
readonly media: { path: string | { dark: string, light: string, hc: string }, altText?: string }
readonly completionEvents?: string[];
/** @deprecated use `completionEvents: 'onCommand:...'` */
readonly doneOn?: { command: string };
readonly when?: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ export class GettingStartedPage extends EditorPane {
stepElement.classList.add('expanded');
stepElement.setAttribute('aria-expanded', 'true');
this.buildMediaComponent(id);
this.gettingStartedService.progressByEvent('stepSelected:' + id);
} else {
this.editorInput.selectedStep = undefined;
}
Expand Down Expand Up @@ -488,7 +489,8 @@ export class GettingStartedPage extends EditorPane {
const path = joinPath(base, src);
const transformed = asWebviewUri({
isExtensionDevelopmentDebug: this.environmentService.isExtensionDevelopment,
...this.environmentService,
webviewResourceRoot: this.environmentService.webviewResourceRoot,
webviewCspSource: this.environmentService.webviewCspSource,
remote: { authority: undefined },
}, this.webviewID, path).toString();
return `src="${transformed}"`;
Expand Down Expand Up @@ -889,6 +891,10 @@ export class GettingStartedPage extends EditorPane {
}
this.openerService.open(command, { allowCommands: true });

if (!isCommand && node.href.startsWith('https://')) {
this.gettingStartedService.progressByEvent('onLink:' + node.href);
}

}, null, this.detailsPageDisposables);

if (isCommand) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo
defaultSnippets: [{
body: {
'id': '$1', 'title': '$2', 'description': '$3',
'doneOn': { 'command': '$5' },
'completionEvents': ['$5'],
'media': { 'path': '$6', 'type': '$7' }
}
}],
Expand Down Expand Up @@ -122,8 +122,38 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo
}
]
},
completionEvents: {
description: localize('walkthroughs.steps.completionEvents', "Events that should trigger this step to become checked off. If empty or not defined, the step will check off when any of the step's buttons or links are clicked; if the step has no buttons or links it will check on when it is selected."),
type: 'array',
items: {
type: 'string',
defaultSnippets: [
{
label: 'onCommand',
description: localize('walkthroughs.steps.completionEvents.onCommand', 'Check off step when a given command is executed anywhere in VS Code.'),
body: 'onCommand:${1:commandId}'
},
{
label: 'onLink',
description: localize('walkthroughs.steps.completionEvents.onLink', 'Check off step when a given link is opened via a Getting Started step.'),
body: 'onLink:${2:linkId}'
},
{
label: 'extensionInstalled',
description: localize('walkthroughs.steps.completionEvents.extensionInstalled', 'Check off step when an extension with the given id is installed. If the extension is already installed, the step will start off checked.'),
body: 'extensionInstalled:${3:extensionId}'
},
{
label: 'stepSelected',
description: localize('walkthroughs.steps.completionEvents.stepSelected', 'Check off step as soon as it is selected.'),
body: 'stepSelected'
},
]
}
},
doneOn: {
description: localize('walkthroughs.steps.doneOn', "Signal to mark step as complete."),
deprecationMessage: localize('walkthroughs.steps.doneOn.deprecation', "doneOn is deprecated, use completionEvents"),
type: 'object',
required: ['command'],
defaultSnippets: [{ 'body': { command: '$1' } }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ import { GettingStartedInput } from 'vs/workbench/contrib/welcome/gettingStarted
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { GettingStartedPage } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { LinkedText, parseLinkedText } from 'vs/base/common/linkedText';
import { ILink, LinkedText, parseLinkedText } from 'vs/base/common/linkedText';
import { walkthroughsExtensionPoint } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedExtensionPoint';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { dirname } from 'vs/base/common/path';
import { coalesce, flatten } from 'vs/base/common/arrays';

export const IGettingStartedService = createDecorator<IGettingStartedService>('gettingStartedService');

Expand All @@ -52,7 +53,9 @@ export interface IGettingStartedStep {
category: GettingStartedCategory | string
when: ContextKeyExpression
order: number
doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never }
/** @deprecated */
doneOn?: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never }
completionEvents: string[]
media:
| { type: 'image', path: { hc: URI, light: URI, dark: URI }, altText: string }
| { type: 'markdown', path: URI, base: URI, root: URI }
Expand Down Expand Up @@ -151,8 +154,8 @@ export class GettingStartedService extends Disposable implements IGettingStarted
private memento: Memento;
private stepProgress: Record<string, StepProgress>;

private commandListeners = new Map<string, string[]>();
private eventListeners = new Map<string, string[]>();
private sessionEvents = new Set<string>();
private completionListeners = new Map<string, Set<string>>();

private gettingStartedContributions = new Map<string, IGettingStartedCategory>();
private steps = new Map<string, IGettingStartedStep>();
Expand Down Expand Up @@ -186,17 +189,22 @@ export class GettingStartedService extends Disposable implements IGettingStarted
removed.forEach(e => this.unregisterExtensionContributions(e.description));
});

this._register(this.commandService.onDidExecuteCommand(command => this.progressByCommand(command.commandId)));
this._register(this.commandService.onDidExecuteCommand(command => this.progressByEvent(`onCommand:${command.commandId}`)));

this.extensionManagementService.getInstalled().then(installed => {
installed.forEach(ext => this.progressByEvent(`extensionInstalled:${ext.identifier.id.toLowerCase()}`));
});

this._register(this.extensionManagementService.onDidInstallExtension(async e => {
if (await this.hostService.hadLastFocus()) {
this.sessionInstalledExtensions.add(e.identifier.id);
}
this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`);
}));

if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); }
if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('onEvent:sync-enabled'); }
this._register(userDataAutoSyncEnablementService.onDidChangeEnablement(() => {
if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('sync-enabled'); }
if (userDataAutoSyncEnablementService.isEnabled()) { this.progressByEvent('onEvent:sync-enabled'); }
}));

startEntries.forEach(async (entry, index) => {
Expand All @@ -221,6 +229,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
this.getStepOverrides(step, category.id);
return ({
...step,
completionEvents: step.completionEvents ?? [],
description: parseDescription(step.description),
category: category.id,
order: index,
Expand Down Expand Up @@ -389,9 +398,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted

return ({
description, media,
doneOn: step.doneOn?.command
? { commandExecuted: step.doneOn.command }
: { eventFired: 'markDone:' + fullyQualifiedID },
completionEvents: step.completionEvents?.filter(x => typeof x === 'string') ?? [],
id: fullyQualifiedID,
title: step.title,
when: ContextKeyExpr.deserialize(step.when) ?? ContextKeyExpr.true(),
Expand Down Expand Up @@ -456,22 +463,71 @@ export class GettingStartedService extends Disposable implements IGettingStarted
}

private registerDoneListeners(step: IGettingStartedStep) {
if (step.doneOn.commandExecuted) {
const existing = this.commandListeners.get(step.doneOn.commandExecuted);
if (existing) { existing.push(step.id); }
else {
this.commandListeners.set(step.doneOn.commandExecuted, [step.id]);
}
if (step.doneOn) {
if (step.doneOn.commandExecuted) { step.completionEvents.push(`onCommand:${step.doneOn.commandExecuted}`); }
if (step.doneOn.eventFired) { step.completionEvents.push(`onEvent:${step.doneOn.eventFired}`); }
}

if (!step.completionEvents.length) {
step.completionEvents = coalesce(flatten(
step.description
.filter(linkedText => linkedText.nodes.length === 1) // only buttons
.map(linkedText =>
linkedText.nodes
.filter(((node): node is ILink => typeof node !== 'string'))
.map(({ href }) => {
if (href.startsWith('command:')) {
return 'onCommand:' + href.slice('command:'.length, href.includes('?') ? href.indexOf('?') : undefined);
}
if (href.startsWith('https://')) {
return 'onLink:' + href;
}
return undefined;
}))));
}
if (step.doneOn.eventFired) {
const existing = this.eventListeners.get(step.doneOn.eventFired);
if (existing) { existing.push(step.id); }
else {
this.eventListeners.set(step.doneOn.eventFired, [step.id]);

if (!step.completionEvents.length) {
step.completionEvents.push('stepSelected');
}

for (let event of step.completionEvents) {
const [_, eventType, argument] = /^([^:]*):?(.*)$/.exec(event) ?? [];

if (!eventType) {
console.error(`Unknown completionEvent ${event} when registering step ${step.id}`);
continue;
}

switch (eventType) {
case 'onLink': case 'onEvent': break;
case 'stepSelected':
event = eventType + ':' + step.id;
break;
case 'onCommand':
event = eventType + ':' + argument.replace(/^toSide:/, '');
break;
case 'extensionInstalled':
event = eventType + ':' + argument.toLowerCase();
break;
default:
console.error(`Unknown completionEvent ${event} when registering step ${step.id}`);
continue;
}

this.registerCompletionListener(event, step);
if (this.sessionEvents.has(event)) {
this.progressStep(step.id);
}
}
}

private registerCompletionListener(event: string, step: IGettingStartedStep) {
if (!this.completionListeners.has(event)) {
this.completionListeners.set(event, new Set());
}
this.completionListeners.get(event)?.add(step.id);
}

getCategories(): IGettingStartedCategoryWithProgress[] {
const registeredCategories = [...this.gettingStartedContributions.values()];
const categoriesWithCompletion = registeredCategories
Expand Down Expand Up @@ -539,14 +595,9 @@ export class GettingStartedService extends Disposable implements IGettingStarted
this._onDidProgressStep.fire(this.getStepProgress(step));
}

private progressByCommand(command: string) {
const listening = this.commandListeners.get(command) ?? [];
listening.forEach(id => this.progressStep(id));
}

progressByEvent(event: string): void {
const listening = this.eventListeners.get(event) ?? [];
listening.forEach(id => this.progressStep(id));
this.sessionEvents.add(event);
this.completionListeners.get(event)?.forEach(id => this.progressStep(id));
}

private registerStartEntry(categoryDescriptor: IGettingStartedStartEntryDescriptor): void {
Expand Down
Loading

0 comments on commit 4c63cdf

Please sign in to comment.