Update walkthrough contribution api to single object model

Ref #119097
This commit is contained in:
Jackson Kearl 2021-03-16 17:17:19 -07:00
parent e8e6d64105
commit f6f5111700
No known key found for this signature in database
GPG key ID: DA09A59C409FC400
4 changed files with 166 additions and 200 deletions

View file

@ -113,22 +113,23 @@ export interface IAuthenticationContribution {
readonly label: string;
}
export interface IWelcomeItem {
export interface IWalkthroughTask {
readonly id: string;
readonly title: string;
readonly description: string;
readonly button: { title: string } & ({ command?: never, link: string } | { command: string, link?: never }),
readonly media: { path: string | { hc: string, light: string, dark: string }, altText: string },
readonly doneOn?:
| { event: string; command?: never }
| { event?: never; command: string };
readonly button:
| { title: string, link: string, command?: never }
| { title: string, command: string, link?: never },
readonly media: { path: string, altText: string },
readonly doneOn?: { command: string };
readonly when?: string;
}
export interface IWelcomeCategory {
export interface IWalkthrough {
readonly id: string,
readonly title: string;
readonly description: string;
readonly tasks: IWalkthroughTask[];
readonly when?: string;
}
@ -151,8 +152,7 @@ export interface IExtensionContributions {
readonly customEditors?: readonly IWebviewEditor[];
readonly codeActions?: readonly ICodeActionContribution[];
authentication?: IAuthenticationContribution[];
welcomeItems?: { [category: string]: IWelcomeItem[] };
welcomeCategories?: IWelcomeCategory[];
walkthroughs?: IWalkthrough[];
}
export type ExtensionKind = 'ui' | 'workspace' | 'web';

View file

@ -10,6 +10,7 @@ import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from
import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode } from 'vs/base/common/keyCodes';
@ -145,3 +146,132 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
},
}
});
ExtensionsRegistry.registerExtensionPoint({
extensionPoint: 'walkthroughs',
jsonSchema: {
doNotSuggest: true,
description: localize('walkthroughs', "Contribute collections of tasks to help users with your extension. Experimental, available in VS Code Insiders only."),
type: 'array',
items: {
type: 'object',
required: ['id', 'title', 'description', 'tasks'],
defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3', 'tasks': [] } }],
properties: {
id: {
type: 'string',
description: localize('walkthroughs.id', "Unique identifier for this walkthrough."),
},
title: {
type: 'string',
description: localize('walkthroughs.title', "Title of walkthrough.")
},
description: {
type: 'string',
description: localize('walkthroughs.description', "Description of walkthrough.")
},
when: {
type: 'string',
description: localize('walkthroughs.when', "Context key expression to control the visibility of this walkthrough.")
},
tasks: {
type: 'array',
description: localize('walkthroughs.tasks', "Tasks to complete as part of this walkthrough."),
items: {
type: 'object',
required: ['id', 'title', 'description', 'button', 'media'],
defaultSnippets: [{
body: {
'id': '$1', 'title': '$2', 'description': '$3',
'button': { 'title': '$4', 'command': '$5' },
'doneOn': { 'command': '$5' },
'media': { 'path': '$6', 'altText': '$7' }
}
}],
properties: {
id: {
type: 'string',
description: localize('walkthroughs.tasks.id', "Unique identifier for this task. This is used to keep track of which tasks have been completed."),
},
title: {
type: 'string',
description: localize('walkthroughs.tasks.title', "Title of task.")
},
description: {
type: 'string',
description: localize('walkthroughs.tasks.description', "Description of task.")
},
button: {
description: localize('walkthroughs.tasks.button', "The task's button, which can either link to an external resource or run a command"),
oneOf: [
{
type: 'object',
required: ['title', 'command'],
defaultSnippets: [{ 'body': { 'title': '$1', 'command': '$2' } }],
properties: {
title: {
type: 'string',
description: localize('walkthroughs.tasks.button.title', "Title of button.")
},
command: {
type: 'string',
description: localize('walkthroughs.tasks.button.command', "Command to run when button is clicked.")
}
}
},
{
type: 'object',
required: ['title', 'link'],
defaultSnippets: [{ 'body': { 'title': '$1', 'link': '$2' } }],
properties: {
title: {
type: 'string',
description: localize('walkthroughs.tasks.button.title', "Title of button.")
},
link: {
type: 'string',
description: localize('walkthroughs.tasks.button.link', "Link to open when button is clicked. Opening this link will mark the task completed.")
}
}
}
]
},
media: {
type: 'object',
required: ['path', 'altText'],
description: localize('walkthroughs.tasks.media', "Image to show alongside this task."),
defaultSnippets: [{ 'body': { 'altText': '$1', 'path': '$2' } }],
properties: {
path: {
description: localize('walkthroughs.tasks.media.path', "Path to an image, relative to extension directory."),
type: 'string',
},
altText: {
type: 'string',
description: localize('walkthroughs.tasks.media.altText', "Alternate text to display when the image cannot be loaded or in screen readers.")
}
}
},
doneOn: {
description: localize('walkthroughs.tasks.doneOn', "Signal to mark task as complete."),
type: 'object',
required: ['command'],
defaultSnippets: [{ 'body': { command: '$1' } }],
properties: {
'command': {
description: localize('walkthroughs.tasks.oneOn.command', "Mark task done when the specified command is executed."),
type: 'string'
}
},
},
when: {
type: 'string',
description: localize('walkthroughs.tasks.when', "Context key expression to control the visibility of this task.")
}
}
}
}
}
}
}
});

View file

@ -7,11 +7,9 @@ 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 { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { content } from 'vs/workbench/services/gettingStarted/common/gettingStartedContent';
import { localize } from 'vs/nls';
export const enum GettingStartedCategory {
Beginner = 'Beginner',
@ -172,161 +170,3 @@ content.forEach(category => {
Registry.add(GettingStartedRegistryID, registryImpl);
export const GettingStartedRegistry: IGettingStartedRegistry = Registry.as(GettingStartedRegistryID);
ExtensionsRegistry.registerExtensionPoint({
extensionPoint: 'welcomeItems',
jsonSchema: {
doNotSuggest: true,
description: localize('gettingStarted', "Contribute items to help users in getting started with your extension. Keys correspond to categories contributed via welcomeCategories contribution point. Experimental, available in VS Code Insiders only."),
type: 'object',
additionalProperties: {
type: 'array',
items: {
type: 'object',
required: ['id', 'title', 'description', 'button', 'media'],
defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3', 'button': { 'title': '$4' }, 'media': { 'path': '$5', 'altText': '$6' } } }],
properties: {
id: {
type: 'string',
description: localize('gettingStarted.id', "Unique identifier for this item."),
},
title: {
type: 'string',
description: localize('gettingStarted.title', "Title of item.")
},
description: {
type: 'string',
description: localize('gettingStarted.description', "Description of item.")
},
button: {
description: localize('gettingStarted.button', "The item's button, which can either link to an external resource or run a command"),
oneOf: [
{
type: 'object',
required: ['title', 'command'],
defaultSnippets: [{ 'body': { 'title': '$1', 'command': '$2' } }],
properties: {
title: {
type: 'string',
description: localize('gettingStarted.button.title', "Title of button.")
},
command: {
type: 'string',
description: localize('gettingStarted.button.command', "Command to run when button is clicked. Running this command will mark the item completed.")
}
}
},
{
type: 'object',
required: ['title', 'link'],
defaultSnippets: [{ 'body': { 'title': '$1', 'link': '$2' } }],
properties: {
title: {
type: 'string',
description: localize('gettingStarted.button.title', "Title of button.")
},
link: {
type: 'string',
description: localize('gettingStarted.button.link', "Link to open when button is clicked. Opening this link will mark the item completed.")
}
}
}
]
},
media: {
type: 'object',
required: ['path', 'altText'],
description: localize('gettingStarted.media', "Image to show alongside this item."),
defaultSnippets: [{ 'body': { 'altText': '$1' } }],
properties: {
path: {
description: localize('gettingStarted.media.path', "Either a single string path to an image to be used on all color themes, or separate paths for light, dark, and high contrast themes."),
oneOf: [
{
type: 'string',
defaultSnippets: [{ 'body': '$1' }],
},
{
type: 'object',
defaultSnippets: [{ 'body': { 'hc': '$1', 'light': '$2', 'dark': '$3' } }],
required: ['hc', 'light', 'dark'],
properties: {
hc: { type: 'string' },
light: { type: 'string' },
dark: { type: 'string' },
}
},
]
},
altText: {
type: 'string',
description: localize('gettingStarted.media.altText', "Alternate text to display when the image cannot be loaded or in screen readers.")
}
}
},
doneOn: {
oneOf: [
{
type: 'object',
required: ['event'],
properties: {
'event': {
description: localize('gettingStarted.oneOn.event', "Mark item done when the specified event is marked via the invoking the `welcomeItems.markEvent` command."),
type: 'string'
}
}
},
{
type: 'object',
required: ['command'],
properties: {
'command': {
description: localize('gettingStarted.oneOn.command', "Mark item done when the specified command is executed."),
type: 'string'
}
}
},
],
description: localize('gettingStarted.doneOn', "Signal to mark item as complete. If not defined, running the button's command will mark the item complete.")
},
when: {
type: 'string',
description: localize('gettingStarted.when', "Context key expression to control the visibility of this getting started item.")
}
}
}
}
}
});
ExtensionsRegistry.registerExtensionPoint({
extensionPoint: 'welcomeCategories',
jsonSchema: {
doNotSuggest: true,
description: localize('welcomeCategories', "Contribute categories of items to help users in getting started with your extension. Items themselves are contributed via welcomeItems contribution point. Experimental, available in VS Code Insiders only."),
type: 'array',
items: {
type: 'object',
required: ['id', 'title', 'description'],
defaultSnippets: [{ body: { 'id': '$1', 'title': '$2', 'description': '$3' } }],
properties: {
id: {
type: 'string',
description: localize('welcomeCategories.id', "Unique identifier for this category."),
},
title: {
type: 'string',
description: localize('welcomeCategories.title', "Title of category.")
},
description: {
type: 'string',
description: localize('welcomeCategories.description', "Description of category.")
},
when: {
type: 'string',
description: localize('welcomeCategories.when', "Context key expression to control the visibility of this category.")
}
}
}
}
});

View file

@ -139,20 +139,17 @@ export class GettingStartedService extends Disposable implements IGettingStarted
if (!this.trackedExtensions.has(ExtensionIdentifier.toKey(extension.identifier))) {
this.trackedExtensions.add(ExtensionIdentifier.toKey(extension.identifier));
if ((extension.contributes?.welcomeCategories || extension.contributes?.welcomeItems) && this.productService.quality === 'stable') {
if ((extension.contributes?.walkthroughs?.length) && this.productService.quality === 'stable') {
console.warn('Extension', extension.identifier.value, 'contributes welcome page content but this is a Stable build and extension contributions are only available in Insiders. The contributed content will be disregarded.');
return;
}
const contributedCategories = new Map();
extension.contributes?.welcomeCategories?.forEach(category => {
const categoryID = extension.identifier.value + '.' + category.id;
contributedCategories.set(category.id, categoryID);
extension.contributes?.walkthroughs?.forEach(section => {
const categoryID = extension.identifier.value + '#' + section.id;
this.registry.registerCategory({
content: { type: 'items' },
description: category.description,
title: category.title,
description: section.description,
title: section.title,
id: categoryID,
icon: {
type: 'image',
@ -160,31 +157,30 @@ export class GettingStartedService extends Disposable implements IGettingStarted
? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true)
: DefaultIconPath
},
when: ContextKeyExpr.deserialize(category.when) ?? ContextKeyExpr.true(),
when: ContextKeyExpr.deserialize(section.when) ?? ContextKeyExpr.true(),
});
});
try {
Object.entries(extension.contributes?.welcomeItems ?? {}).forEach(([category, items]) =>
items.forEach((item, index) =>
this.registry.registerTask({
button: item.button,
description: item.description,
media: { type: 'image', altText: item.media.altText, path: convertPaths(item.media.path) },
doneOn: item.doneOn?.event
? { eventFired: item.doneOn.event }
: item.doneOn?.command ?
{ commandExecuted: item.doneOn.command }
: item.button.command
? { commandExecuted: item.button.command }
: { eventFired: `linkOpened:${item.button.link}` },
id: extension.identifier.value + '.' + item.id,
title: item.title,
when: ContextKeyExpr.deserialize(item.when) ?? ContextKeyExpr.true(),
category: contributedCategories.get(category) ?? category,
order: index,
})
)
);
section.tasks.forEach((task, index) =>
this.registry.registerTask({
button: task.button,
description: task.description,
media: { type: 'image', altText: task.media.altText, path: convertPaths(task.media.path) },
doneOn: task.doneOn?.command
? { commandExecuted: task.doneOn.command }
: task.button.command
? { commandExecuted: task.button.command }
: { eventFired: `linkOpened:${task.button.link}` },
id: extension.identifier.value + '#' + task.id,
title: task.title,
when: ContextKeyExpr.deserialize(task.when) ?? ContextKeyExpr.true(),
category: categoryID,
order: index,
}));
} catch (e) {
console.error('Error registering walkthrough tasks for ', categoryID, e);
}
});
}
}