Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/actions/build-vsix/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ runs:
run: npm run updateBuildNumber -- --buildNumber $GITHUB_RUN_ID
shell: bash

- name: Update extension dependencies
run: npm run addExtensionDependencies
shell: bash

- name: Update Optional extension dependencies
- name: Update optional extension dependencies
run: npm run addExtensionPackDependencies
shell: bash

Expand Down
8 changes: 6 additions & 2 deletions build/license-header.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
PLEASE NOTE: This Python extension for Visual Studio Code has a hard dependency on the Jupyter extension for Visual Studio Code which is installed automatically alongside it. The Python extension for Visual Studio Code also holds an optional dependency on the Pylance extension for Visual Studio Code, which is also installed automatically but is separately licensed.
PLEASE NOTE: This is the license for the Python extension for Visual Studio Code. The Python extension automatically installs other extensions as optional dependencies, which can be uninstalled at any time. These extensions have separate licenses:

All the source code for the Python extension for Visual Studio Code is available under the MIT License (given below) as is the source code for the Jupyter extension for Visual Studio Code. But the optional Pylance extension for Visual Studio Code is only available in binary form and it is not licensed under the MIT License. The Pylance extension for Visual Studio Code is licensed under a Microsoft proprietary license, the terms of which are available here: https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license.
- The Jupyter extension is released under an MIT License:
https://marketplace.visualstudio.com/items/ms-toolsai.jupyter/license

- The Pylance extension is only available in binary form and is released under a Microsoft proprietary license, the terms of which are available here:
https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license

------------------------------------------------------------------------------
17 changes: 1 addition & 16 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,32 +77,17 @@ gulp.task('webpack', async () => {
await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.config.js', 'extension');
});

gulp.task('addExtensionDependencies', async () => {
await addExtensionDependencies();
});

gulp.task('addExtensionPackDependencies', async () => {
await buildLicense();
await addExtensionPackDependencies();
});

async function addExtensionDependencies() {
// Update the package.json to add extension dependencies at build time so that
// extension dependencies need not be installed during development
const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8');
const packageJson = JSON.parse(packageJsonContents);
packageJson.extensionDependencies = ['ms-toolsai.jupyter'].concat(
packageJson.extensionDependencies ? packageJson.extensionDependencies : [],
);
await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8');
}

async function addExtensionPackDependencies() {
// Update the package.json to add extension pack dependencies at build time so that
// extension dependencies need not be installed during development
const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8');
const packageJson = JSON.parse(packageJsonContents);
packageJson.extensionPack = ['ms-python.vscode-pylance'].concat(
packageJson.extensionPack = ['ms-toolsai.jupyter', 'ms-python.vscode-pylance'].concat(
packageJson.extensionPack ? packageJson.extensionPack : [],
);
await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8');
Expand Down
1 change: 1 addition & 0 deletions news/1 Enhancements/16102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Move the Jupyter extension from being a hard dependency to an optional one, and display an informational prompt if Jupyter commands try to be executed from the Start Page.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2088,7 +2088,6 @@
"format-check": "prettier --check 'src/**/*.ts' 'src/**/*.tsx' 'build/**/*.js' '.github/**/*.yml' gulpfile.js",
"format-fix": "prettier --write 'src/**/*.ts' 'src/**/*.tsx' 'build/**/*.js' '.github/**/*.yml' gulpfile.js",
"clean": "gulp clean",
"addExtensionDependencies": "gulp addExtensionDependencies",
"addExtensionPackDependencies": "gulp addExtensionPackDependencies",
"updateBuildNumber": "gulp updateBuildNumber",
"verifyBundle": "gulp verifyBundle",
Expand Down
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
"StartPage.createAPythonFile": "Create a Python File",
"StartPage.pythonFileDescription": "- Create a <div class=\"link\" role=\"button\" onclick={0}>new file</div> with a .py extension",
"StartPage.openInteractiveWindow": "Use the Interactive Window to develop Python Scripts",
"StartPage.interactiveWindowDesc": "- You can create cells on a Python file by typing \"#%%\" <br /> - Use \"<div class=\"italics\">Shift + Enter</div> \" to run a cell, the output will be shown in the interactive window",
"StartPage.interactiveWindowDesc": "- You can create cells on a Python file by typing \"#%%\". Make sure you have the Jupyter extension installed. <br /> - Use \"<div class=\"italics\">Shift + Enter</div> \" to run a cell, the output will be shown in the interactive window",
"StartPage.releaseNotes": "Take a look at our <a class=\"link\" href={0}>Release Notes</a> to learn more about the latest features.",
"StartPage.mailingList": "<a class=\"link\" href={0}>Sign up</a> for tips and tutorials through our mailing list.",
"StartPage.tutorialAndDoc": "Explore more features in our <a class=\"link\" href={0}>Tutorials</a> or check <a class=\"link\" href={1}>Documentation</a> for tips and troubleshooting.",
Expand All @@ -233,6 +233,7 @@
"StartPage.folderDesc": "- Open a <div class=\"link\" role=\"button\" onclick={0}>Folder</div><br /> - Open a <div class=\"link\" role=\"button\" onclick={1}>Workspace</div>",
"StartPage.badWebPanelFormatString": "<html><body><h1>{0} is not a valid file name</h1></body></html>",
"Jupyter.extensionRequired": "The Jupyter extension is required to perform that task. Click Yes to open the Jupyter extension installation page.",
"Jupyter.extensionNotInstalled": "This feature is available in the Jupyter extension, which isn't currently installed.",
"TensorBoard.missingSourceFile": "We could not locate the requested source file on disk. Please manually specify the file.",
"TensorBoard.selectMissingSourceFile": "Choose File",
"TensorBoard.selectMissingSourceFileDescription": "The source file's contents may not match the original contents in the trace.",
Expand Down
15 changes: 4 additions & 11 deletions src/client/common/application/commandManager.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { injectable } from 'inversify';
import { commands, Disposable, TextEditor, TextEditorEdit } from 'vscode';
import { ICommandNameArgumentTypeMapping } from './commands';
import { ICommandManager, IJupyterExtensionDependencyManager } from './types';
import { ICommandManager } from './types';

@injectable()
export class CommandManager implements ICommandManager {
constructor(
@inject(IJupyterExtensionDependencyManager)
private jupyterExtensionDependencyManager: IJupyterExtensionDependencyManager,
) {}
constructor() {}

/**
* Registers a command that can be invoked via a keyboard shortcut,
Expand Down Expand Up @@ -73,11 +70,7 @@ export class CommandManager implements ICommandManager {
E extends keyof ICommandNameArgumentTypeMapping,
U extends ICommandNameArgumentTypeMapping[E]
>(command: E, ...rest: U): Thenable<T | undefined> {
if (command.includes('jupyter') && !this.jupyterExtensionDependencyManager.isJupyterExtensionInstalled) {
return this.jupyterExtensionDependencyManager.installJupyterExtension(this);
} else {
return commands.executeCommand<T>(command, ...rest);
}
return commands.executeCommand<T>(command, ...rest);
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,6 @@ export interface ICommandManager {
export const IJupyterExtensionDependencyManager = Symbol('IJupyterExtensionDependencyManager');
export interface IJupyterExtensionDependencyManager {
readonly isJupyterExtensionInstalled: boolean;
installJupyterExtension(commandManager: ICommandManager): Promise<undefined>;
}

export const IDocumentManager = Symbol('IDocumentManager');
Expand Down
6 changes: 6 additions & 0 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ import {
} from './types';
import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput';
import { Random } from './utils/random';
import { JupyterNotInstalledNotificationHelper } from '../jupyter/jupyterNotInstalledNotificationHelper';
import { IJupyterNotInstalledNotificationHelper } from '../jupyter/types';

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS);
Expand All @@ -140,6 +142,10 @@ export function registerTypes(serviceManager: IServiceManager) {
IJupyterExtensionDependencyManager,
JupyterExtensionDependencyManager,
);
serviceManager.addSingleton<IJupyterNotInstalledNotificationHelper>(
IJupyterNotInstalledNotificationHelper,
JupyterNotInstalledNotificationHelper,
);
serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager);
serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService);
serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService);
Expand Down
94 changes: 67 additions & 27 deletions src/client/common/startPage/startPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,27 @@

'use strict';

import { inject, injectable } from 'inversify';
import { inject, injectable, named } from 'inversify';
import * as path from 'path';
import { ConfigurationTarget, EventEmitter, UIKind, Uri, ViewColumn } from 'vscode';
import { IExtensionSingleActivationService } from '../../activation/types';
import { EXTENSION_ROOT_DIR } from '../../constants';
import { IJupyterNotInstalledNotificationHelper, JupyterNotInstalledOrigin } from '../../jupyter/types';
import { sendTelemetryEvent } from '../../telemetry';
import {
IApplicationEnvironment,
IApplicationShell,
ICommandManager,
IDocumentManager,
IJupyterExtensionDependencyManager,
IWebviewPanelProvider,
IWorkspaceService,
} from '../application/types';
import { CommandSource } from '../constants';
import { CommandSource, STANDARD_OUTPUT_CHANNEL } from '../constants';
import { IFileSystem } from '../platform/types';
import { IConfigurationService, IExtensionContext, Resource } from '../types';
import { IConfigurationService, IExtensionContext, IOutputChannel, Resource } from '../types';
import * as localize from '../utils/localize';
import { Jupyter } from '../utils/localize';
import { StopWatch } from '../utils/stopWatch';
import { Telemetry } from './constants';
import { StartPageMessageListener } from './startPageMessageListener';
Expand Down Expand Up @@ -62,6 +65,10 @@ export class StartPage extends WebviewPanelHost<IStartPageMapping>
@inject(IApplicationShell) private appShell: IApplicationShell,
@inject(IExtensionContext) private readonly context: IExtensionContext,
@inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment,
@inject(IJupyterNotInstalledNotificationHelper)
private notificationHelper: IJupyterNotInstalledNotificationHelper,
@inject(IJupyterExtensionDependencyManager) private depsManager: IJupyterExtensionDependencyManager,
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel,
) {
super(
configuration,
Expand Down Expand Up @@ -128,6 +135,9 @@ export class StartPage extends WebviewPanelHost<IStartPageMapping>
}

public async onMessage(message: string, payload: unknown): Promise<void> {
const shouldShowJupyterNotInstalledPrompt = await this.notificationHelper.shouldShowJupypterExtensionNotInstalledPrompt();
const isJupyterInstalled = this.depsManager.isJupyterExtensionInstalled;

switch (message) {
case StartPageMessages.Started:
this.webviewDidLoad = true;
Expand All @@ -140,19 +150,29 @@ export class StartPage extends WebviewPanelHost<IStartPageMapping>
break;
}
case StartPageMessages.OpenBlankNotebook: {
sendTelemetryEvent(Telemetry.StartPageOpenBlankNotebook);
this.setTelemetryFlags();

const savedVersion: string | undefined = this.context.globalState.get(EXTENSION_VERSION_MEMENTO);

if (savedVersion) {
await this.commandManager.executeCommand(
'jupyter.opennotebook',
undefined,
CommandSource.commandPalette,
);
if (!isJupyterInstalled) {
this.output.appendLine(Jupyter.jupyterExtensionNotInstalled());

if (shouldShowJupyterNotInstalledPrompt) {
await this.notificationHelper.showJupyterNotInstalledPrompt(
JupyterNotInstalledOrigin.StartPageOpenBlankNotebook,
);
}
} else {
this.openSampleNotebook().ignoreErrors();
sendTelemetryEvent(Telemetry.StartPageOpenBlankNotebook);
this.setTelemetryFlags();

const savedVersion: string | undefined = this.context.globalState.get(EXTENSION_VERSION_MEMENTO);

if (savedVersion) {
await this.commandManager.executeCommand(
'jupyter.opennotebook',
undefined,
CommandSource.commandPalette,
);
} else {
this.openSampleNotebook().ignoreErrors();
}
}
break;
}
Expand All @@ -168,15 +188,25 @@ export class StartPage extends WebviewPanelHost<IStartPageMapping>
break;
}
case StartPageMessages.OpenInteractiveWindow: {
sendTelemetryEvent(Telemetry.StartPageOpenInteractiveWindow);
this.setTelemetryFlags();

const doc2 = await this.documentManager.openTextDocument({
language: 'python',
content: `#%%\nprint("${localize.StartPage.helloWorld()}")`,
});
await this.documentManager.showTextDocument(doc2, 1, true);
await this.commandManager.executeCommand('jupyter.runallcells', Uri.parse(''));
if (!isJupyterInstalled) {
this.output.appendLine(Jupyter.jupyterExtensionNotInstalled());

if (shouldShowJupyterNotInstalledPrompt) {
await this.notificationHelper.showJupyterNotInstalledPrompt(
JupyterNotInstalledOrigin.StartPageOpenInteractiveWindow,
);
}
} else {
sendTelemetryEvent(Telemetry.StartPageOpenInteractiveWindow);
this.setTelemetryFlags();

const doc2 = await this.documentManager.openTextDocument({
language: 'python',
content: `#%%\nprint("${localize.StartPage.helloWorld()}")`,
});
await this.documentManager.showTextDocument(doc2, 1, true);
await this.commandManager.executeCommand('jupyter.runallcells', doc2.uri);
}
break;
}
case StartPageMessages.OpenCommandPalette:
Expand All @@ -192,10 +222,20 @@ export class StartPage extends WebviewPanelHost<IStartPageMapping>
await this.commandManager.executeCommand('workbench.action.quickOpen', '>Create New Blank Notebook');
break;
case StartPageMessages.OpenSampleNotebook:
sendTelemetryEvent(Telemetry.StartPageOpenSampleNotebook);
this.setTelemetryFlags();
if (!isJupyterInstalled) {
this.output.appendLine(Jupyter.jupyterExtensionNotInstalled());

if (shouldShowJupyterNotInstalledPrompt) {
await this.notificationHelper.showJupyterNotInstalledPrompt(
JupyterNotInstalledOrigin.StartPageOpenSampleNotebook,
);
}
} else {
sendTelemetryEvent(Telemetry.StartPageOpenSampleNotebook);
this.setTelemetryFlags();

this.openSampleNotebook().ignoreErrors();
this.openSampleNotebook().ignoreErrors();
}
break;
case StartPageMessages.OpenFileBrowser: {
sendTelemetryEvent(Telemetry.StartPageOpenFileBrowser);
Expand Down
8 changes: 4 additions & 4 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ export namespace Pylance {
}

export namespace Jupyter {
export const jupyterExtensionRequired = localize(
'Jupyter.extensionRequired',
'The Jupyter extension is required to perform that task. Click Yes to open the Jupyter extension installation page.',
export const jupyterExtensionNotInstalled = localize(
'Jupyter.extensionNotInstalled',
"This feature is available in the Jupyter extension, which isn't currently installed.",
);
}

Expand Down Expand Up @@ -441,7 +441,7 @@ export namespace StartPage {
);
export const interactiveWindowDesc = localize(
'StartPage.interactiveWindowDesc',
'- You can create cells on a Python file by typing "#%%" <br /> - Use "<div class="italics">Shift + Enter</div> " to run a cell, the output will be shown in the interactive window',
'- You can create cells on a Python file by typing "#%%". Make sure you have the Jupyter extension installed. <br /> - Use "<div class="italics">Shift + Enter</div> " to run a cell, the output will be shown in the interactive window',
);

export const releaseNotes = localize(
Expand Down
18 changes: 2 additions & 16 deletions src/client/jupyter/jupyterExtensionDependencyManager.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import { inject, injectable } from 'inversify';
import { IApplicationShell, ICommandManager, IJupyterExtensionDependencyManager } from '../common/application/types';
import { IJupyterExtensionDependencyManager } from '../common/application/types';
import { JUPYTER_EXTENSION_ID } from '../common/constants';
import { IExtensions } from '../common/types';
import { Common, Jupyter } from '../common/utils/localize';

@injectable()
export class JupyterExtensionDependencyManager implements IJupyterExtensionDependencyManager {
constructor(
@inject(IExtensions) private extensions: IExtensions,
@inject(IApplicationShell) private appShell: IApplicationShell,
) {}
constructor(@inject(IExtensions) private extensions: IExtensions) {}

public get isJupyterExtensionInstalled(): boolean {
return this.extensions.getExtension(JUPYTER_EXTENSION_ID) !== undefined;
}

public async installJupyterExtension(commandManager: ICommandManager): Promise<undefined> {
const yes = Common.bannerLabelYes();
const no = Common.bannerLabelNo();
const answer = await this.appShell.showErrorMessage(Jupyter.jupyterExtensionRequired(), yes, no);
if (answer === yes) {
commandManager.executeCommand('extension.open', JUPYTER_EXTENSION_ID);
}
return undefined;
}
}
Loading