Skip to content

Commit 27f575c

Browse files
authored
Better getting started experience for Jupyter (microsoft#152380)
* Initial run of Jupyter new experience * Add ability to skip a walkthrough on install * Try different wording in quickpick * Fix walkthrough code * Update demo * Adjust wording * Better comment * Remove some old code * Extra spacing * Allow passing context through extension install * Await notebook extension activation * Fix quick input * Address sandeep comment * Address comments
1 parent c84655d commit 27f575c

File tree

8 files changed

+159
-28
lines changed

8 files changed

+159
-28
lines changed

src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
238238
} catch (error) { /* ignore */ }
239239
}
240240
}
241-
installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source });
241+
installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context });
242242
} catch (error) {
243243
if (!URI.isUri(task.source)) {
244244
reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error });
@@ -277,7 +277,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
277277
}
278278
}
279279

280-
this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source })));
280+
this._onDidInstallExtensions.fire(allInstallExtensionTasks.map(({ task }) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: options.context })));
281281
throw error;
282282
} finally {
283283
/* Remove the gallery tasks from the cache */

src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export interface InstallExtensionResult {
352352
readonly operation: InstallOperation;
353353
readonly source?: URI | IGalleryExtension;
354354
readonly local?: ILocalExtension;
355+
readonly context?: IStringDictionary<any>;
355356
}
356357

357358
export interface DidUninstallExtensionEvent {
@@ -384,7 +385,18 @@ export class ExtensionManagementError extends Error {
384385
}
385386
}
386387

387-
export type InstallOptions = { isBuiltin?: boolean; isMachineScoped?: boolean; donotIncludePackAndDependencies?: boolean; installGivenVersion?: boolean; installPreReleaseVersion?: boolean; operation?: InstallOperation };
388+
export type InstallOptions = {
389+
isBuiltin?: boolean;
390+
isMachineScoped?: boolean;
391+
donotIncludePackAndDependencies?: boolean;
392+
installGivenVersion?: boolean;
393+
installPreReleaseVersion?: boolean;
394+
operation?: InstallOperation;
395+
/**
396+
* Context passed through to InstallExtensionResult
397+
*/
398+
context?: IStringDictionary<any>;
399+
};
388400
export type InstallVSIXOptions = Omit<InstallOptions, 'installGivenVersion'> & { installOnlyNewlyAddedFromExtensionPack?: boolean };
389401
export type UninstallOptions = { donotIncludePack?: boolean; donotCheckDependents?: boolean };
390402

src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,7 +1201,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
12011201
return false;
12021202
}
12031203

1204-
install(extension: URI | IExtension, installOptions?: InstallOptions | InstallVSIXOptions): Promise<IExtension> {
1204+
install(extension: URI | IExtension, installOptions?: InstallOptions | InstallVSIXOptions, progressLocation?: ProgressLocation): Promise<IExtension> {
12051205
if (extension instanceof URI) {
12061206
return this.installWithProgress(() => this.installFromVSIX(extension, installOptions));
12071207
}
@@ -1216,7 +1216,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
12161216
return Promise.reject(new Error('Missing gallery'));
12171217
}
12181218

1219-
return this.installWithProgress(() => this.installFromGallery(extension, gallery, installOptions), gallery.displayName);
1219+
return this.installWithProgress(() => this.installFromGallery(extension, gallery, installOptions), gallery.displayName, progressLocation);
12201220
}
12211221

12221222
setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void> {
@@ -1314,10 +1314,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
13141314
return extension;
13151315
}
13161316

1317-
private installWithProgress<T>(installTask: () => Promise<T>, extensionName?: string): Promise<T> {
1317+
private installWithProgress<T>(installTask: () => Promise<T>, extensionName?: string, progressLocation?: ProgressLocation): Promise<T> {
13181318
const title = extensionName ? nls.localize('installing named extension', "Installing '{0}' extension....", extensionName) : nls.localize('installing extension', 'Installing extension....');
13191319
return this.progressService.withProgress({
1320-
location: ProgressLocation.Extensions,
1320+
location: progressLocation ?? ProgressLocation.Extensions,
13211321
title
13221322
}, () => installTask());
13231323
}

src/vs/workbench/contrib/extensions/common/extensions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { IView, IViewPaneContainer } from 'vs/workbench/common/views';
1717
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
1818
import { IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions';
1919
import { IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput';
20+
import { ProgressLocation } from 'vs/platform/progress/common/progress';
2021

2122
export const VIEWLET_ID = 'workbench.view.extensions';
2223

@@ -101,7 +102,7 @@ export interface IExtensionsWorkbenchService {
101102
getExtensions(extensionInfos: IExtensionInfo[], options: IExtensionQueryOptions, token: CancellationToken): Promise<IExtension[]>;
102103
canInstall(extension: IExtension): Promise<boolean>;
103104
install(vsix: URI, installOptions?: InstallVSIXOptions): Promise<IExtension>;
104-
install(extension: IExtension, installOptions?: InstallOptions): Promise<IExtension>;
105+
install(extension: IExtension, installOptions?: InstallOptions, progressLocation?: ProgressLocation): Promise<IExtension>;
105106
uninstall(extension: IExtension): Promise<void>;
106107
installVersion(extension: IExtension, version: string, installOptions?: InstallOptions): Promise<IExtension>;
107108
reinstall(extension: IExtension): Promise<IExtension>;

src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ import { Registry } from 'vs/platform/registry/common/platform';
1919
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
2020
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
2121
import { ViewContainerLocation } from 'vs/workbench/common/views';
22-
import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
22+
import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
2323
import { CENTER_ACTIVE_CELL } from 'vs/workbench/contrib/notebook/browser/contrib/navigation/arrow';
2424
import { NOTEBOOK_ACTIONS_CATEGORY, SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
2525
import { NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
26-
import { getNotebookEditorFromEditorPane, INotebookEditor, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
26+
import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookExtensionRecommendation, KERNEL_RECOMMENDATIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
2727
import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
2828
import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons';
2929
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
@@ -34,6 +34,11 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
3434
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
3535
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
3636
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
37+
import { CancellationToken } from 'vs/base/common/cancellation';
38+
import { ProgressLocation } from 'vs/platform/progress/common/progress';
39+
import { IProductService } from 'vs/platform/product/common/productService';
40+
import { Codicon } from 'vs/base/common/codicons';
41+
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
3742

3843
registerAction2(class extends Action2 {
3944
constructor() {
@@ -103,10 +108,13 @@ registerAction2(class extends Action2 {
103108
): Promise<boolean> {
104109
const notebookKernelService = accessor.get(INotebookKernelService);
105110
const editorService = accessor.get(IEditorService);
111+
const productService = accessor.get(IProductService);
106112
const quickInputService = accessor.get(IQuickInputService);
107113
const labelService = accessor.get(ILabelService);
108114
const logService = accessor.get(ILogService);
109115
const paneCompositeService = accessor.get(IPaneCompositePartService);
116+
const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService);
117+
const extensionHostService = accessor.get(IExtensionService);
110118

111119
let editor: INotebookEditor | undefined;
112120
if (context !== undefined && 'notebookEditorId' in context) {
@@ -229,11 +237,25 @@ registerAction2(class extends Action2 {
229237
});
230238
}
231239

240+
let suggestedExtension: INotebookExtensionRecommendation | undefined;
232241
if (!all.length && !sourceActions.length) {
242+
const activeNotebookModel = getNotebookEditorFromEditorPane(editorService.activeEditorPane)?.textModel;
243+
if (activeNotebookModel) {
244+
const language = this.getSuggestedLanguage(activeNotebookModel);
245+
suggestedExtension = language ? this.getSuggestedKernelFromLanguage(activeNotebookModel.viewType, language) : undefined;
246+
}
247+
if (suggestedExtension) {
248+
// We have a suggested kernel, show an option to install it
249+
quickPickItems.push({
250+
id: 'installSuggested',
251+
description: suggestedExtension.displayName ?? suggestedExtension.extensionId,
252+
label: nls.localize('installSuggestedKernel', '$({0}) Install suggested extensions', Codicon.lightbulb.id),
253+
});
254+
}
233255
// there is no kernel, show the install from marketplace
234256
quickPickItems.push({
235257
id: 'install',
236-
label: nls.localize('installKernels', "Install kernels from the marketplace"),
258+
label: nls.localize('searchForKernels', "Browse marketplace for kernel extensions"),
237259
});
238260
}
239261

@@ -258,7 +280,22 @@ registerAction2(class extends Action2 {
258280
// actions
259281

260282
if (pick.id === 'install') {
261-
await this._showKernelExtension(paneCompositeService, notebook.viewType);
283+
await this._showKernelExtension(
284+
paneCompositeService,
285+
extensionWorkbenchService,
286+
extensionHostService,
287+
notebook.viewType
288+
);
289+
// suggestedExtension must be defined for this option to be shown, but still check to make TS happy
290+
} else if (pick.id === 'installSuggested' && suggestedExtension) {
291+
await this._showKernelExtension(
292+
paneCompositeService,
293+
extensionWorkbenchService,
294+
extensionHostService,
295+
notebook.viewType,
296+
suggestedExtension.extensionId,
297+
productService.quality !== 'stable'
298+
);
262299
} else if ('action' in pick) {
263300
// selected explicilty, it should trigger the execution?
264301
pick.action.runAction();
@@ -268,17 +305,69 @@ registerAction2(class extends Action2 {
268305
return false;
269306
}
270307

271-
private async _showKernelExtension(paneCompositePartService: IPaneCompositePartService, viewType: string) {
272-
const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true);
273-
const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined;
308+
/**
309+
* Examine the most common language in the notebook
310+
* @param notebookTextModel The notebook text model
311+
* @returns What the suggested language is for the notebook. Used for kernal installing
312+
*/
313+
private getSuggestedLanguage(notebookTextModel: NotebookTextModel): string | undefined {
314+
const metaData = notebookTextModel.metadata;
315+
let suggestedKernelLanguage: string | undefined = (metaData.custom as any)?.metadata?.language_info?.name;
316+
// TODO how do we suggest multi language notebooks?
317+
if (!suggestedKernelLanguage) {
318+
const cellLanguages = notebookTextModel.cells.map(cell => cell.language).filter(language => language !== 'markdown');
319+
// Check if cell languages is all the same
320+
if (cellLanguages.length > 1) {
321+
const firstLanguage = cellLanguages[0];
322+
if (cellLanguages.every(language => language === firstLanguage)) {
323+
suggestedKernelLanguage = firstLanguage;
324+
}
325+
}
326+
}
327+
return suggestedKernelLanguage;
328+
}
274329

275-
const extId = KERNEL_EXTENSIONS.get(viewType);
330+
/**
331+
* Given a language and notebook view type suggest a kernel for installation
332+
* @param language The language to find a suggested kernel extension for
333+
* @returns A recommednation object for the recommended extension, else undefined
334+
*/
335+
private getSuggestedKernelFromLanguage(viewType: string, language: string): INotebookExtensionRecommendation | undefined {
336+
const recommendation = KERNEL_RECOMMENDATIONS.get(viewType)?.get(language);
337+
return recommendation;
338+
}
339+
340+
private async _showKernelExtension(
341+
paneCompositePartService: IPaneCompositePartService,
342+
extensionWorkbenchService: IExtensionsWorkbenchService,
343+
extensionService: IExtensionService,
344+
viewType: string,
345+
extId?: string,
346+
isInsiders?: boolean
347+
) {
348+
// If extension id is provided attempt to install the extension as the user has requested the suggested ones be installed
276349
if (extId) {
277-
view?.search(`@id:${extId}`);
278-
} else {
279-
const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join('');
280-
view?.search(`@tag:notebookKernel${pascalCased}`);
350+
const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0];
351+
const canInstall = await extensionWorkbenchService.canInstall(extension);
352+
// If we can install then install it, otherwise we will fall out into searching the viewlet
353+
if (canInstall) {
354+
await extensionWorkbenchService.install(
355+
extension,
356+
{
357+
installPreReleaseVersion: isInsiders ?? false,
358+
context: { skipWalkthrough: true }
359+
},
360+
ProgressLocation.Notification
361+
);
362+
await extensionService.activateByEvent(`onNotebook:${viewType}`);
363+
return;
364+
}
281365
}
366+
367+
const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true);
368+
const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined;
369+
const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join('');
370+
view?.search(`@tag:notebookKernel${pascalCased}`);
282371
}
283372
});
284373

src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
4949
export const KERNEL_EXTENSIONS = new Map<string, string>([
5050
[IPYNB_VIEW_TYPE, JUPYTER_EXTENSION_ID],
5151
]);
52+
// @TODO lramos15, place this in a similar spot to our normal recommendations.
53+
export const KERNEL_RECOMMENDATIONS = new Map<string, Map<string, INotebookExtensionRecommendation>>();
54+
KERNEL_RECOMMENDATIONS.set(IPYNB_VIEW_TYPE, new Map<string, INotebookExtensionRecommendation>());
55+
KERNEL_RECOMMENDATIONS.get(IPYNB_VIEW_TYPE)?.set('python', {
56+
extensionId: 'ms-python.python',
57+
displayName: 'Python + Jupyter',
58+
});
59+
60+
export interface INotebookExtensionRecommendation {
61+
extensionId: string;
62+
displayName?: string;
63+
}
5264

5365
//#endregion
5466

src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from 'vs/base/
3030
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
3131
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
3232
import { StartupPageContribution, } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage';
33+
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
3334

3435

3536
export * as icons from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons';
@@ -49,7 +50,11 @@ registerAction2(class extends Action2 {
4950
});
5051
}
5152

52-
public run(accessor: ServicesAccessor, walkthroughID: string | { category: string; step: string } | undefined, toSide: boolean | undefined) {
53+
public run(
54+
accessor: ServicesAccessor,
55+
walkthroughID: string | { category: string; step: string } | undefined,
56+
toSide: boolean | undefined
57+
) {
5358
const editorGroupsService = accessor.get(IEditorGroupsService);
5459
const instantiationService = accessor.get(IInstantiationService);
5560
const editorService = accessor.get(IEditorService);
@@ -80,8 +85,19 @@ registerAction2(class extends Action2 {
8085
}
8186
}
8287

83-
// Otherwise, just make a new one.
84-
editorService.openEditor(instantiationService.createInstance(GettingStartedInput, { selectedCategory: selectedCategory, selectedStep: selectedStep }), {}, toSide ? SIDE_GROUP : undefined);
88+
const activeEditor = editorService.activeEditor;
89+
const gettingStartedInput = instantiationService.createInstance(GettingStartedInput, { selectedCategory: selectedCategory, selectedStep: selectedStep });
90+
// If it's the extension install page then lets replace it with the getting started page
91+
if (activeEditor instanceof ExtensionsInput) {
92+
const activeGroup = editorGroupsService.activeGroup;
93+
activeGroup.replaceEditors([{
94+
editor: activeEditor,
95+
replacement: gettingStartedInput
96+
}]);
97+
} else {
98+
// else open respecting toSide
99+
editorService.openEditor(gettingStartedInput, { preserveFocus: toSide ?? false }, toSide ? SIDE_GROUP : undefined);
100+
}
85101
} else {
86102
editorService.openEditor(new GettingStartedInput({}), {});
87103
}

src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ
136136
private steps = new Map<string, IWalkthroughStep>();
137137

138138
private tasExperimentService?: IWorkbenchAssignmentService;
139-
private sessionInstalledExtensions = new Set<string>();
139+
private sessionInstalledExtensions: Set<string> = new Set<string>();
140140

141141
private categoryVisibilityContextKeys = new Set<string>();
142142
private stepCompletionContextKeyExpressions = new Set<ContextKeyExpression>();
@@ -235,7 +235,9 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ
235235
this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => {
236236
const hadLastFoucs = await this.hostService.hadLastFocus();
237237
for (const e of result) {
238-
if (hadLastFoucs) {
238+
// If the window had last focus and the install didn't specify to skip the walkthrough
239+
// Then add it to the sessionInstallExtensions to be opened
240+
if (hadLastFoucs && !e?.context?.skipWalkthrough) {
239241
this.sessionInstalledExtensions.add(e.identifier.id.toLowerCase());
240242
}
241243
this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`);
@@ -425,20 +427,19 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ
425427

426428
this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.PROFILE, StorageTarget.USER);
427429

428-
429430
if (sectionToOpen && this.configurationService.getValue<string>('workbench.welcomePage.walkthroughs.openOnInstall')) {
430431
type GettingStartedAutoOpenClassification = {
431432
id: {
432433
classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight';
433-
owner: 'JacksonKearl';
434+
owner: 'lramos15';
434435
comment: 'Used to understand what walkthroughs are consulted most frequently';
435436
};
436437
};
437438
type GettingStartedAutoOpenEvent = {
438439
id: string;
439440
};
440441
this.telemetryService.publicLog2<GettingStartedAutoOpenEvent, GettingStartedAutoOpenClassification>('gettingStarted.didAutoOpenWalkthrough', { id: sectionToOpen });
441-
this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen);
442+
this.commandService.executeCommand('workbench.action.openWalkthrough', sectionToOpen, true);
442443
}
443444
}
444445

0 commit comments

Comments
 (0)