Add GettingStartedService/Registry and initial getting started UI (#111175)

* WIP -Getting Started Registry

* Initial idea of how registry/service will work

* Getting started with Getting Started

* Add telemetry and touch up enablement flag

* Add contrib as startup editor

* Move to allowing static Getting Start blob

* No newline

* Remove unused enum
This commit is contained in:
Jackson Kearl 2020-11-25 14:05:49 -08:00 committed by GitHub
parent ef03adf3a0
commit 0921f711c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1272 additions and 14 deletions

View file

@ -373,6 +373,10 @@
{
"name": "vs/workbench/services/extensionRecommendations",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/services/gettingStarted",
"project": "vscode-workbench"
}
]
}

View file

@ -1373,8 +1373,8 @@ export function safeInnerHtml(node: HTMLElement, value: string): void {
const options = _extInsaneOptions({
allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'],
allowedAttributes: {
'a': ['href'],
'button': ['data-href'],
'a': ['href', 'x-dispatch'],
'button': ['data-href', 'x-dispatch'],
'input': ['type', 'placeholder', 'checked', 'required'],
'label': ['for'],
'select': ['required'],

View file

@ -22,6 +22,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData';
import { IGettingStartedService } from 'vs/workbench/services/gettingStarted/common/gettingStartedService';
export class SelectColorThemeAction extends Action {
@ -34,6 +35,7 @@ export class SelectColorThemeAction extends Action {
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IGettingStartedService private readonly gettingStartedService: IGettingStartedService,
@IViewletService private readonly viewletService: IViewletService
) {
super(id, label);
@ -84,6 +86,7 @@ export class SelectColorThemeAction extends Action {
openExtensionViewlet(this.viewletService, `category:themes ${quickpick.value}`);
} else {
selectTheme(theme, true);
this.gettingStartedService.progressByEvent('themeSelected');
}
isCompleted = true;
quickpick.hide();

View file

@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* 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 { GettingStartedInputFactory, GettingStartedPage } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
import product from 'vs/platform/product/common/product';
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.showGettingStarted',
title: localize('Getting Started', "Getting Started"),
category: localize('help', "Help"),
f1: true,
precondition: ContextKeyExpr.has('config.workbench.experimental.gettingStarted'),
menu: {
id: MenuId.MenubarHelpMenu,
when: ContextKeyExpr.has('config.workbench.experimental.gettingStarted'),
group: '1_welcome',
order: 2,
}
});
}
public run(accessor: ServicesAccessor) {
return accessor.get(IInstantiationService).createInstance(GettingStartedPage).openEditor();
}
});
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(GettingStartedInputFactory.ID, GettingStartedInputFactory);
if (product.quality !== 'stable') {
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
...workbenchConfigurationNodeBase,
properties: {
'workbench.experimental.gettingStarted': {
type: 'boolean',
description: localize('gettingStartedDescription', "Enables an experimental Getting Started page, accesible via the Help menu."),
default: false,
}
}
});
}

View file

@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.file-icons-enabled .show-file-icons .vs_code_editor_getting_started\.md-name-file-icon.md-ext-file-icon.ext-file-icon.markdown-lang-file-icon.file-icon::before {
content: ' ';
background-image: url('../../../../browser/media/code-icon.svg');
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide {
width: 100%;
position: absolute;
transition: left 0.25s;
left: 0;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .header {
text-align: center;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .title {
display: inline-block;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-title {
margin-top: 6px;
margin-bottom: 2px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-description-container {
width: 100%
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-description {
font-size: 12pt;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-progress {
margin-top: 6px;
font-size: 8pt;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories progress {
font-size: 12pt;
width: 100%;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories #getting-started-categories-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 70%;
max-width: 900px;
margin: 20px auto;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .getting-started-category {
width: 330px;
height: 100px;
text-align: left;
display: flex;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .getting-started-category {
padding-right: 46px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon {
margin: 10px 8px 0 0;
font-size: 32px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-category {
width: 330px;
height: 100px;
display: flex;
margin-left: 12px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-category .codicon {
margin-left:0;
margin-top: 28px;
font-size: 22pt;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail #getting-started-detail-columns {
display: flex;
justify-content: center;
padding: 30px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task {
width: 100%;
height: 26pt;
overflow: hidden;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task.expanded {
width: 100%;
height: unset;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-description-container {
padding-left: 30px;
padding-right: 4px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-title {
margin-bottom: 4px;
font-size: 14pt;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-description {
font-size: 11pt;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-next {
float: right;
margin-top: 16px;
margin-right: 25px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .codicon.hidden {
display: none;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .codicon {
float: left;
font-size: 20pt;
position: relative;
top: -4px;
left: -1px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task-action {
margin: 10px 0 0;
padding: 4px 8px;
font-size: 11pt;
min-width: 100px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail #getting-started-detail-left {
min-width: 330px;
width: 33%;
max-width: 400px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail #getting-started-detail-right {
width: 66%;
text-align: center;
padding: 35px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button {
border: none;
margin: 10px;
color: inherit;
text-align: left;
padding: 10px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .prev-button {
position: absolute;
left: 0;
font-size: 12pt;
margin: 10px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .prev-button .codicon {
position: relative;
top: 2px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .product-icon {
background-image: url('../../../../browser/media/code-icon.svg');
width: 75px;
height: 75px;
display: inline-block;
margin-right: 20px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .skip {
display: block;
width: 150px;
margin: 0 auto;
text-align: center;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide h1 {
font-size: 32pt;
font-weight: normal;
border-bottom: none;
margin-bottom: 0;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide h2 {
font-weight: normal;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide h3 {
font-weight: normal;
margin-top: 0;
margin-bottom: 0;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .subtitle {
font-size: 18pt;
margin-top: 0;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .footer {
position: absolute;
text-align: center;
bottom: 0;
width: 100%;
margin-bottom: 20px;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.next {
left: 100%;
}
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.prev {
left: -100%;
}

View file

@ -0,0 +1,394 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./gettingStarted';
import 'vs/workbench/contrib/welcome/gettingStarted/browser/vs_code_editor_getting_started';
import { localize } from 'vs/nls';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { assertIsDefined } from 'vs/base/common/types';
import { $, addDisposableListener } from 'vs/base/browser/dom';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IProductService } from 'vs/platform/product/common/productService';
import { IGettingStartedCategoryWithProgress, IGettingStartedService } from 'vs/workbench/services/gettingStarted/common/gettingStartedService';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { buttonBackground as welcomeButtonBackground, buttonHoverBackground as welcomeButtonHoverBackground, welcomePageBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors';
import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, descriptionForeground, focusBorder, foreground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
export const gettingStartedInputTypeId = 'workbench.editors.gettingStartedInput';
const telemetryFrom = 'gettingStartedPage';
export class GettingStartedPage extends Disposable {
readonly editorInput: WalkThroughInput;
private inProgressScroll = Promise.resolve();
private dispatchListeners = new DisposableStore();
private gettingStartedCategories: IGettingStartedCategoryWithProgress[];
private currentCategory: IGettingStartedCategoryWithProgress | undefined;
constructor(
@IEditorService private readonly editorService: IEditorService,
@ICommandService private readonly commandService: ICommandService,
@IProductService private readonly productService: IProductService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IGettingStartedService private readonly gettingStartedService: IGettingStartedService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IInstantiationService private readonly instantiationService: IInstantiationService) {
super();
const resource = FileAccess.asBrowserUri('./vs_code_editor_getting_started.md', require)
.with({
scheme: Schemas.walkThrough,
query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcome/gettingStarted/browser/vs_code_editor_getting_started' })
});
this.editorInput = this.instantiationService.createInstance(WalkThroughInput, {
typeId: gettingStartedInputTypeId,
name: localize('editorGettingStarted.title', "Getting Started"),
resource,
telemetryFrom,
onReady: (container: HTMLElement) => this.onReady(container)
});
this.gettingStartedCategories = this.gettingStartedService.getCategories();
this._register(this.dispatchListeners);
this._register(this.gettingStartedService.onDidAddTask(task => console.log('added new task', task, 'that isnt being rendered yet')));
this._register(this.gettingStartedService.onDidAddCategory(category => console.log('added new category', category, 'that isnt being rendered yet')));
this._register(this.gettingStartedService.onDidProgressTask(task => {
const category = this.gettingStartedCategories.find(category => category.id === task.category);
if (!category) { throw Error('Could not find category with ID: ' + task.category); }
if (category.content.type !== 'items') { throw Error('internaal error: progressing task in a non-items category'); }
const ourTask = category.content.items.find(_task => _task.id === task.id);
if (!ourTask) {
throw Error('Could not find task with ID: ' + task.id);
}
ourTask.done = task.done;
if (category.id === this.currentCategory?.id) {
const badgeelement = assertIsDefined(document.getElementById('done-task-' + task.id));
if (task.done) {
badgeelement.classList.remove('codicon-star-empty');
badgeelement.classList.add('codicon-star-full');
}
else {
badgeelement.classList.add('codicon-star-empty');
badgeelement.classList.remove('codicon-star-full');
}
}
this.updateCategoryProgress();
}));
}
public openEditor(options: IEditorOptions = { pinned: true }) {
return this.editorService.openEditor(this.editorInput, options);
}
private registerDispatchListeners(container: HTMLElement) {
this.dispatchListeners.clear();
container.querySelectorAll('[x-dispatch]').forEach(element => {
const [command, argument] = (element.getAttribute('x-dispatch') ?? '').split(':');
if (command) {
this.dispatchListeners.add(addDisposableListener(element, 'click', (e) => {
type GettingStartedActionClassification = {
command: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
argument: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
type GettingStartedActionEvent = {
command: string;
argument: string | undefined;
};
this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command, argument });
switch (command) {
case 'scrollPrev': {
this.scrollPrev(container);
break;
}
case 'skip': {
this.commandService.executeCommand('workbench.action.closeActiveEditor');
break;
}
case 'selectCategory': {
const selectedCategory = this.gettingStartedCategories.find(category => category.id === argument);
if (!selectedCategory) { throw Error('Could not find category with ID ' + argument); }
if (selectedCategory.content.type === 'command') {
this.commandService.executeCommand(selectedCategory.content.command);
} else {
this.scrollToCategory(container, argument);
}
break;
}
case 'selectTask': {
this.selectTask(argument);
e.stopPropagation();
break;
}
case 'runTaskAction': {
if (!this.currentCategory || this.currentCategory.content.type !== 'items') {
throw Error('cannot run task action for category of non items type' + this.currentCategory?.id);
}
const taskToRun = assertIsDefined(this.currentCategory?.content.items.find(task => task.id === argument));
const commandToRun = assertIsDefined(taskToRun.button?.command);
this.commandService.executeCommand(commandToRun);
break;
}
default: {
console.error('Dispatch to', command, argument, 'not defined');
break;
}
}
}));
}
});
}
private selectTask(id: string | undefined) {
const mediaElement = assertIsDefined(document.getElementById('getting-started-media'));
if (id) {
const taskElement = assertIsDefined(document.getElementById('getting-started-task-' + id));
if (!this.currentCategory || this.currentCategory.content.type !== 'items') {
throw Error('cannot expand task for category of non items type' + this.currentCategory?.id);
}
const taskToExpand = assertIsDefined(this.currentCategory.content.items.find(task => task.id === id));
mediaElement.setAttribute('src', taskToExpand.media.toString());
taskElement.parentElement?.querySelectorAll('.expanded').forEach(node => node.classList.remove('expanded'));
taskElement.classList.add('expanded');
} else {
mediaElement.setAttribute('src', '');
}
}
private onReady(container: HTMLElement) {
const categoryElements = this.gettingStartedCategories.map(
category => {
const categoryDescriptionElement =
category.content.type === 'items' ?
$('.category-description-container', {},
$('h3.category-title', {}, category.title),
$('.category-description.description', {}, category.description),
$('.category-progress', { 'x-data-category-id': category.id, }, $('.message'), $('progress'))) :
$('.category-description-container', {},
$('h3.category-title', {}, category.title),
$('.category-description.description', {}, category.description));
return $('button.getting-started-category',
{ 'x-dispatch': 'selectCategory:' + category.id },
$('.codicon.codicon-' + category.codicon, {}), categoryDescriptionElement);
});
const rightColumn = assertIsDefined(container.querySelector('#getting-started-detail-right'));
rightColumn.appendChild($('img#getting-started-media'));
categoryElements.forEach(element => {
assertIsDefined(document.getElementById('getting-started-categories-container')).appendChild(element);
});
this.updateCategoryProgress();
assertIsDefined(document.getElementById('product-name')).textContent = this.productService.nameLong;
this.registerDispatchListeners(container);
}
private updateCategoryProgress() {
document.querySelectorAll('.category-progress').forEach(element => {
const categoryID = element.getAttribute('x-data-category-id');
const category = this.gettingStartedCategories.find(category => category.id === categoryID);
if (!category) { throw Error('Could not find c=ategory with ID ' + categoryID); }
if (category.content.type !== 'items') { throw Error('Category with ID ' + categoryID + ' is not of items type'); }
const numDone = category.content.items.filter(task => task.done).length;
const numTotal = category.content.items.length;
const message = assertIsDefined(element.firstChild);
const bar = assertIsDefined(element.lastChild) as HTMLProgressElement;
bar.value = numDone;
bar.max = numTotal;
if (numTotal === numDone) {
message.textContent = `All items complete!`;
}
else {
message.textContent = `${numDone} of ${numTotal} items complete`;
}
});
}
private async scrollToCategory(container: HTMLElement, categoryID: string) {
this.inProgressScroll = this.inProgressScroll.then(async () => {
this.clearDetialView();
this.currentCategory = this.gettingStartedCategories.find(category => category.id === categoryID);
if (!this.currentCategory) { throw Error('could not find category with ID ' + categoryID); }
if (this.currentCategory.content.type !== 'items') { throw Error('category with ID ' + categoryID + ' is not of items type'); }
const slides = [...container.querySelectorAll('.gettingStartedSlide').values()];
const currentSlide = slides.findIndex(element =>
!element.classList.contains('prev') && !element.classList.contains('next'));
if (currentSlide < slides.length - 1) {
slides[currentSlide].classList.add('prev');
const detailSlide = assertIsDefined(slides[currentSlide + 1]);
detailSlide.classList.remove('next');
const detailTitle = assertIsDefined(document.getElementById('getting-started-detail-title'));
detailTitle.appendChild(
$('.getting-started-category',
{},
$('.codicon.codicon-' + this.currentCategory.codicon, {}),
$('.category-description-container', {},
$('h2.category-title', {}, this.currentCategory.title),
$('.category-description.description', {}, this.currentCategory.description))));
const categoryElements = this.currentCategory.content.items.map(
(task, i, arr) =>
$('button.getting-started-task',
{ 'x-dispatch': 'selectTask:' + task.id, id: 'getting-started-task-' + task.id },
$('.codicon' + (task.done ? '.codicon-star-full' : '.codicon-star-empty'), { id: 'done-task-' + task.id },),
$('.task-description-container', {},
$('h3.task-title', {}, task.title),
$('.task-description.description', {}, task.description),
...(
task.button
? [$('button.emphasis.getting-started-task-action', { 'x-dispatch': 'runTaskAction:' + task.id },
task.button.title + this.getKeybindingLabel(task.button.command)
)]
: []),
...(
arr[i + 1]
? [
$('a.task-next',
{ 'x-dispatch': 'selectTask:' + arr[i + 1].id }, localize('next', "Next")),
] : []
)
)));
const detailContainer = assertIsDefined(document.getElementById('getting-started-detail-container'));
categoryElements.forEach(element => detailContainer.appendChild(element));
const toExpand = this.currentCategory.content.items.find(item => !item.done) ?? this.currentCategory.content.items[0];
this.selectTask(toExpand.id);
this.registerDispatchListeners(container);
}
});
}
private clearDetialView() {
const detailContainer = assertIsDefined(document.getElementById('getting-started-detail-container'));
while (detailContainer.firstChild) { detailContainer.removeChild(detailContainer.firstChild); }
const detailTitle = assertIsDefined(document.getElementById('getting-started-detail-title'));
while (detailTitle.firstChild) { detailTitle.removeChild(detailTitle.firstChild); }
}
private getKeybindingLabel(command: string) {
const binding = this.keybindingService.lookupKeybinding(command);
if (!binding) { return ''; }
else { return ` (${binding.getLabel()})`; }
}
private async scrollPrev(container: HTMLElement) {
this.inProgressScroll = this.inProgressScroll.then(async () => {
this.currentCategory = undefined;
this.selectTask(undefined);
const slides = [...container.querySelectorAll('.gettingStartedSlide').values()];
const currentSlide = slides.findIndex(element =>
!element.classList.contains('prev') && !element.classList.contains('next'));
if (currentSlide > 0) {
slides[currentSlide].classList.add('next');
assertIsDefined(slides[currentSlide - 1]).classList.remove('prev');
}
});
}
}
export class GettingStartedInputFactory implements IEditorInputFactory {
static readonly ID = gettingStartedInputTypeId;
public canSerialize(editorInput: EditorInput): boolean {
return true;
}
public serialize(editorInput: EditorInput): string {
return '{}';
}
public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): WalkThroughInput {
return instantiationService.createInstance(GettingStartedPage).editorInput;
}
}
registerThemingParticipant((theme, collector) => {
const backgroundColor = theme.getColor(welcomePageBackground);
if (backgroundColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .welcomePageContainer { background-color: ${backgroundColor}; }`);
}
const foregroundColor = theme.getColor(foreground);
if (foregroundColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer { color: ${foregroundColor}; }`);
}
const descriptionColor = theme.getColor(descriptionForeground);
if (descriptionColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .description { color: ${descriptionColor}; }`);
}
const buttonColor = getExtraColor(theme, welcomeButtonBackground, { dark: 'rgba(0, 0, 0, .2)', extra_dark: 'rgba(200, 235, 255, .042)', light: 'rgba(0,0,0,.04)', hc: 'black' });
if (buttonColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button { background: ${buttonColor}; }`);
}
const buttonHoverColor = getExtraColor(theme, welcomeButtonHoverBackground, { dark: 'rgba(200, 235, 255, .072)', extra_dark: 'rgba(200, 235, 255, .072)', light: 'rgba(0,0,0,.10)', hc: null });
if (buttonHoverColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button:hover { background: ${buttonHoverColor}; }`);
}
if (buttonColor && buttonHoverColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button.expanded:hover { background: ${buttonColor}; }`);
}
const emphasisButtonForeground = theme.getColor(buttonForeground);
if (emphasisButtonForeground) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button.emphasis { color: ${emphasisButtonForeground}; }`);
}
const emphasisButtonBackground = theme.getColor(buttonBackground);
if (emphasisButtonBackground) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button.emphasis { background: ${emphasisButtonBackground}; }`);
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .getting-started-category .codicon { color: ${emphasisButtonBackground} }`);
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .codicon { color: ${emphasisButtonBackground} } `);
}
const emphasisButtonHoverBackground = theme.getColor(buttonHoverBackground);
if (emphasisButtonHoverBackground) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button.emphasis:hover { background: ${emphasisButtonHoverBackground}; }`);
}
const link = theme.getColor(textLinkForeground);
if (link) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer a { color: ${link}; }`);
}
const activeLink = theme.getColor(textLinkActiveForeground);
if (activeLink) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer a:hover,
.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer a:active { color: ${activeLink}; }`);
}
const focusColor = theme.getColor(focusBorder);
if (focusColor) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer a:focus { outline-color: ${focusColor}; }`);
}
const border = theme.getColor(contrastBorder);
if (border) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button { border-color: ${border}; border: 1px solid; }`);
}
const activeBorder = theme.getColor(activeContrastBorder);
if (activeBorder) {
collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button:hover { outline-color: ${activeBorder}; }`);
}
});

View file

@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { escape } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
export default () => `
<div class="gettingStartedContainer">
<div class="gettingStarted" role="document">
<div class="gettingStartedSlide categories">
<div class="header">
<div class="product-icon"></div>
<div class="title">
<h1 class="caption"><span id="product-name">${escape(localize('gettingStarted.vscode', "Visual Studio Code"))}</span></h1>
<p class="subtitle description">${escape(localize({ key: 'gettingStarted.editingRedefined', comment: ['Shown as subtitle on the Welcome page.'] }, "Code editing. Redefined"))}</p>
</div>
</div>
<div id="getting-started-categories-container"></div>
</div>
<div class="gettingStartedSlide detail next">
<a class="prev-button" x-dispatch="scrollPrev"><span
class="scroll-button codicon codicon-chevron-left"></span>Back</a>
<div id="getting-started-detail-columns">
<div id="getting-started-detail-left">
<div id="getting-started-detail-title"></div>
<div id="getting-started-detail-container"></div>
</div>
<div id="getting-started-detail-right">
</div>
</div>
</div>
<div class="footer">
<a class="skip" x-dispatch="skip">Skip</a>
</div>
</div>
</div>
`.replace(/\|/g, '`');

View file

@ -13,6 +13,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur
import { IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
import product from 'vs/platform/product/common/product';
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
.registerConfiguration({
@ -21,13 +22,21 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
'workbench.startupEditor': {
'scope': ConfigurationScope.APPLICATION, // Make sure repositories cannot trigger opening a README for tracking.
'type': 'string',
'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench'],
'enumDescriptions': [
'enum': [
...['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench'],
...(product.quality !== 'stable'
? ['gettingStarted']
: [])
],
'enumDescriptions': [...[
localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."),
localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page (default)."),
localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise."),
localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty workspace)."),
localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."),
localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."),],
...(product.quality !== 'stable'
? [localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.gettingStarted' }, "Open the Getting Started page (experimental).")]
: [])
],
'default': 'welcomePage',
'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.")

View file

@ -29,7 +29,7 @@ import { ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/
import { Disposable } from 'vs/base/common/lifecycle';
import { splitName } from 'vs/base/common/labels';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils';
import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor';
@ -46,6 +46,8 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { gettingStartedInputTypeId, GettingStartedPage } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted';
import { buttonBackground, buttonHoverBackground, welcomePageBackground } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors';
const configurationKey = 'workbench.startupEditor';
const oldConfigurationKey = 'workbench.welcome.enabled';
@ -69,7 +71,8 @@ export class WelcomePageContribution implements IWorkbenchContribution {
backupFileService.hasBackups().then(hasBackups => {
// Open the welcome even if we opened a set of default editors
if ((!editorService.activeEditor || layoutService.openedDefaultEditors) && !hasBackups) {
const openWithReadme = configurationService.getValue(configurationKey) === 'readme';
const startupEditorSetting = configurationService.getValue(configurationKey) as string;
const openWithReadme = startupEditorSetting === 'readme';
if (openWithReadme) {
return Promise.all(contextService.getWorkspace().folders.map(folder => {
const folderUri = folder.uri;
@ -101,18 +104,21 @@ export class WelcomePageContribution implements IWorkbenchContribution {
return undefined;
});
} else {
const startupEditorTypeID = startupEditorSetting === 'gettingStarted' ? gettingStartedInputTypeId : welcomeInputTypeId;
const startupEditorCtor = startupEditorSetting === 'gettingStarted' ? GettingStartedPage : WelcomePage;
let options: IEditorOptions;
let editor = editorService.activeEditor;
if (editor) {
// Ensure that the welcome editor won't get opened more than once
if (editor.getTypeId() === welcomeInputTypeId || editorService.editors.some(e => e.getTypeId() === welcomeInputTypeId)) {
if (editor.getTypeId() === startupEditorTypeID || editorService.editors.some(e => e.getTypeId() === startupEditorTypeID)) {
return undefined;
}
options = { pinned: false, index: 0 };
} else {
options = { pinned: false };
}
return instantiationService.createInstance(WelcomePage).openEditor(options);
return instantiationService.createInstance(startupEditorCtor).openEditor(options);
}
}
return undefined;
@ -129,7 +135,7 @@ function isWelcomePageEnabled(configurationService: IConfigurationService, conte
return welcomeEnabled.value;
}
}
return startupEditor.value === 'welcomePage' || startupEditor.value === 'readme' || startupEditor.value === 'welcomePageInEmptyWorkbench' && contextService.getWorkbenchState() === WorkbenchState.EMPTY;
return startupEditor.value === 'welcomePage' || startupEditor.value === 'gettingStarted' || startupEditor.value === 'readme' || startupEditor.value === 'welcomePageInEmptyWorkbench' && contextService.getWorkbenchState() === WorkbenchState.EMPTY;
}
export class WelcomePageAction extends Action {
@ -639,10 +645,6 @@ export class WelcomeInputFactory implements IEditorInputFactory {
// theming
export const buttonBackground = registerColor('welcomePage.buttonBackground', { dark: null, light: null, hc: null }, localize('welcomePage.buttonBackground', 'Background color for the buttons on the Welcome page.'));
export const buttonHoverBackground = registerColor('welcomePage.buttonHoverBackground', { dark: null, light: null, hc: null }, localize('welcomePage.buttonHoverBackground', 'Hover background color for the buttons on the Welcome page.'));
export const welcomePageBackground = registerColor('welcomePage.background', { light: null, dark: null, hc: null }, localize('welcomePage.background', 'Background color for the Welcome page.'));
registerThemingParticipant((theme, collector) => {
const backgroundColor = theme.getColor(welcomePageBackground);
if (backgroundColor) {

View file

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { localize } from 'vs/nls';
// Seprate from main module to break dependency cycles between welcomePage and gettingStarted.
export const buttonBackground = registerColor('welcomePage.buttonBackground', { dark: null, light: null, hc: null }, localize('welcomePage.buttonBackground', 'Background color for the buttons on the Welcome page.'));
export const buttonHoverBackground = registerColor('welcomePage.buttonHoverBackground', { dark: null, light: null, hc: null }, localize('welcomePage.buttonHoverBackground', 'Hover background color for the buttons on the Welcome page.'));
export const welcomePageBackground = registerColor('welcomePage.background', { light: null, dark: null, hc: null }, localize('welcomePage.background', 'Background color for the Welcome page.'));

View file

@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
type GettingStartedItem = {
id: string
title: string,
description: string,
button: { title: string, command: string },
doneOn: { commandExecuted: string, eventFired?: never } | { eventFired: string, commandExecuted?: never, }
when?: string,
media: 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: 'Square.png'
},
{
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: 'Tall.png',
},
{
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: 'Short.png',
},
{
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: 'Square.png'
},
{
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: 'Square.png'
}
]
}
},
{
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: 'https://code.visualstudio.com/assets/updates/1_51/custom-tree-hover.gif',
}
]
}
},
{
id: 'Advanced',
title: localize('gettingStarted.advanced.title', "Tips & Tricks"),
codicon: 'tools',
description: localize('gettingStarted.advanced.description', "Favorites from VS Code experts"),
content: {
type: 'items',
items: []
}
},
{
id: 'OpenFolder-Mac',
title: localize('gettingStarted.openFolder.title', "Open Folder"),
codicon: 'folder-opened',
when: 'isMac',
description: localize('gettingStarted.openFolder.description', "Open a project and start working"),
content: {
type: 'command',
command: 'workbench.action.files.openFileFolder'
}
},
{
id: 'OpenFolder-Other',
title: localize('gettingStarted.openFolder.title', "Open Folder"),
codicon: 'folder-opened',
description: localize('gettingStarted.openFolder.description', "Open a project and start working"),
when: '!isMac',
content: {
type: 'command',
command: 'workbench.action.files.openFolder'
}
},
{
id: 'InteractivePlayground',
title: localize('gettingStarted.playground.title', "Interactive Playground"),
codicon: 'library',
description: localize('gettingStarted.interactivePlayground.description', "Learn essential editor features"),
content: {
type: 'command',
command: 'workbench.action.showInteractivePlayground'
}
}
];

View file

@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* 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: URI
}
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: item.media.startsWith('https://')
? URI.parse(item.media, true)
: FileAccess.asFileUri('vs/workbench/services/gettingStarted/common/media/' + item.media, require)
});
});
}
});
Registry.add(GettingStartedRegistryID, registryImpl);
export const GettingStartedRegistry: IGettingStartedRegistry = Registry.as(GettingStartedRegistryID);

View file

@ -0,0 +1,196 @@
/*---------------------------------------------------------------------------------------------
* 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) ?? [];
console.log(event, listening, this.eventListeners);
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: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -276,6 +276,7 @@ import 'vs/workbench/contrib/surveys/browser/languageSurveys.contribution';
// Welcome
import 'vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay';
import 'vs/workbench/contrib/welcome/page/browser/welcomePage.contribution';
import 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution';
import 'vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution';
// Call Hierarchy