Skip to content

Commit 6fa97fc

Browse files
authored
Prompt to reload VS Code when changing jupyter server connecti… (#10402)
* Prompt to reload VS Code * Reload only if required * Fix tests * Add news entry * Oops
1 parent 4250485 commit 6fa97fc

9 files changed

Lines changed: 155 additions & 4 deletions

File tree

news/2 Fixes/9945.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prompt to reload VS Code when changing the Jupyter Server connection.

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
"ExtensionChannels.downloadCompletedOutputMessage": "Insiders build download complete.",
157157
"ExtensionChannels.startingDownloadOutputMessage": "Starting download for Insiders build.",
158158
"Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?",
159+
"DataScience.reloadAfterChangingJupyterServerConnection": "Please reload VS Code when changing the Jupyter Server connection.",
159160
"DataScience.restartKernelMessage": "Do you want to restart the IPython kernel? All variables will be lost.",
160161
"DataScience.restartKernelMessageYes": "Yes",
161162
"DataScience.restartKernelMessageNo": "No",

src/client/common/application/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
8383
export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping {
8484
['workbench.extensions.installExtension']: [Uri | 'ms-python.python'];
8585
['setContext']: [string, boolean];
86+
['python.reloadVSCode']: [string];
8687
['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }];
8788
['python._loadLanguageServerExtension']: {}[];
8889
['python.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken];
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { IExtensionSingleActivationService } from '../../../activation/types';
8+
import { Common } from '../../utils/localize';
9+
import { noop } from '../../utils/misc';
10+
import { IApplicationShell, ICommandManager } from '../types';
11+
12+
/**
13+
* Prompts user to reload VS Code with a custom message, and reloads if necessary.
14+
*/
15+
@injectable()
16+
export class ReloadVSCodeCommandHandler implements IExtensionSingleActivationService {
17+
constructor(
18+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
19+
@inject(IApplicationShell) private readonly appShell: IApplicationShell
20+
) {}
21+
public async activate(): Promise<void> {
22+
this.commandManager.registerCommand('python.reloadVSCode', this.onReloadVSCode, this);
23+
}
24+
private async onReloadVSCode(message: string) {
25+
const item = await this.appShell.showInformationMessage(message, Common.reload());
26+
if (item === Common.reload()) {
27+
this.commandManager.executeCommand('workbench.action.reloadWindow').then(noop, noop);
28+
}
29+
}
30+
}

src/client/common/serviceRegistry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IImportTracker } from '../telemetry/types';
1010
import { ApplicationEnvironment } from './application/applicationEnvironment';
1111
import { ApplicationShell } from './application/applicationShell';
1212
import { CommandManager } from './application/commandManager';
13+
import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand';
1314
import { CustomEditorService } from './application/customEditorService';
1415
import { DebugService } from './application/debugService';
1516
import { DebugSessionTelemetry } from './application/debugSessionTelemetry';
@@ -179,6 +180,10 @@ export function registerTypes(serviceManager: IServiceManager) {
179180
IExtensionSingleActivationService,
180181
InsidersExtensionService
181182
);
183+
serviceManager.addSingleton<IExtensionSingleActivationService>(
184+
IExtensionSingleActivationService,
185+
ReloadVSCodeCommandHandler
186+
);
182187
serviceManager.addSingleton<IExtensionChannelService>(IExtensionChannelService, ExtensionChannelService);
183188
serviceManager.addSingleton<IExtensionChannelRule>(
184189
IExtensionChannelRule,

src/client/common/utils/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ export namespace DataScience {
459459
'DataScience.jupyterNotebookConnectFailed',
460460
'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}'
461461
);
462+
export const reloadAfterChangingJupyterServerConnection = localize(
463+
'DataScience.reloadAfterChangingJupyterServerConnection',
464+
'Please reload VS Code when changing the Jupyter Server connection.'
465+
);
462466
export const jupyterNotebookRemoteConnectFailed = localize(
463467
'DataScience.jupyterNotebookRemoteConnectFailed',
464468
'Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}'

src/client/datascience/jupyter/serverSelector.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
import { inject, injectable, named } from 'inversify';
77
import { ConfigurationTarget, Memento, QuickPickItem } from 'vscode';
8+
import { ICommandManager } from '../../common/application/types';
89
import { GLOBAL_MEMENTO, IConfigurationService, IMemento } from '../../common/types';
910
import { DataScience } from '../../common/utils/localize';
11+
import { noop } from '../../common/utils/misc';
1012
import {
1113
IMultiStepInput,
1214
IMultiStepInputFactory,
@@ -28,7 +30,8 @@ export class JupyterServerSelector {
2830
constructor(
2931
@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento,
3032
@inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory,
31-
@inject(IConfigurationService) private configuration: IConfigurationService
33+
@inject(IConfigurationService) private configuration: IConfigurationService,
34+
@inject(ICommandManager) private cmdManager: ICommandManager
3235
) {}
3336

3437
@captureTelemetry(Telemetry.SelectJupyterURI)
@@ -69,22 +72,38 @@ export class JupyterServerSelector {
6972

7073
@captureTelemetry(Telemetry.SetJupyterURIToLocal)
7174
private async setJupyterURIToLocal(): Promise<void> {
75+
const previousValue = this.configuration.getSettings(undefined).datascience.jupyterServerURI;
7276
await this.configuration.updateSetting(
7377
'dataScience.jupyterServerURI',
7478
Settings.JupyterServerLocalLaunch,
7579
undefined,
7680
ConfigurationTarget.Workspace
7781
);
82+
83+
// Reload if there's a change
84+
if (previousValue !== Settings.JupyterServerLocalLaunch) {
85+
this.cmdManager
86+
.executeCommand('python.reloadVSCode', DataScience.reloadAfterChangingJupyterServerConnection())
87+
.then(noop, noop);
88+
}
7889
}
7990

8091
@captureTelemetry(Telemetry.SetJupyterURIToUserSpecified)
8192
private async setJupyterURIToRemote(userURI: string): Promise<void> {
93+
const previousValue = this.configuration.getSettings(undefined).datascience.jupyterServerURI;
8294
await this.configuration.updateSetting(
8395
'dataScience.jupyterServerURI',
8496
userURI,
8597
undefined,
8698
ConfigurationTarget.Workspace
8799
);
100+
101+
// Reload if there's a change
102+
if (previousValue !== userURI) {
103+
this.cmdManager
104+
.executeCommand('python.reloadVSCode', DataScience.reloadAfterChangingJupyterServerConnection())
105+
.then(noop, noop);
106+
}
88107
}
89108
private validateSelectJupyterURI = async (inputText: string): Promise<string | undefined> => {
90109
try {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';
7+
import { ApplicationShell } from '../../../../client/common/application/applicationShell';
8+
import { CommandManager } from '../../../../client/common/application/commandManager';
9+
import { ReloadVSCodeCommandHandler } from '../../../../client/common/application/commands/reloadCommand';
10+
import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types';
11+
import { Common } from '../../../../client/common/utils/localize';
12+
13+
// Defines a Mocha test suite to group tests of similar kind together
14+
suite('Common Commands ReloadCommand', () => {
15+
let reloadCommandHandler: ReloadVSCodeCommandHandler;
16+
let appShell: IApplicationShell;
17+
let cmdManager: ICommandManager;
18+
setup(async () => {
19+
appShell = mock(ApplicationShell);
20+
cmdManager = mock(CommandManager);
21+
reloadCommandHandler = new ReloadVSCodeCommandHandler(instance(cmdManager), instance(appShell));
22+
when(cmdManager.executeCommand(anything())).thenResolve();
23+
await reloadCommandHandler.activate();
24+
});
25+
26+
test('Confirm command handler is added', async () => {
27+
verify(cmdManager.registerCommand('python.reloadVSCode', anything(), anything())).once();
28+
});
29+
test('Display prompt to reload VS Code with message passed into command', async () => {
30+
const message = 'Hello World!';
31+
// tslint:disable-next-line: no-any
32+
const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function;
33+
34+
await commandHandler.call(reloadCommandHandler, message);
35+
36+
verify(appShell.showInformationMessage(message, Common.reload())).once();
37+
});
38+
test('Do not reload VS Code if user selects `Reload` option', async () => {
39+
const message = 'Hello World!';
40+
// tslint:disable-next-line: no-any
41+
const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function;
42+
// tslint:disable-next-line: no-any
43+
when(appShell.showInformationMessage(message, Common.reload())).thenResolve(Common.reload() as any);
44+
45+
await commandHandler.call(reloadCommandHandler, message);
46+
47+
verify(appShell.showInformationMessage(message, Common.reload())).once();
48+
verify(cmdManager.executeCommand('workbench.action.reloadWindow')).once();
49+
});
50+
test('Do not reload VS Code if user does not select `Reload` option', async () => {
51+
const message = 'Hello World!';
52+
// tslint:disable-next-line: no-any
53+
const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function;
54+
when(appShell.showInformationMessage(message, Common.reload())).thenResolve();
55+
56+
await commandHandler.call(reloadCommandHandler, message);
57+
58+
verify(appShell.showInformationMessage(message, Common.reload())).once();
59+
verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never();
60+
});
61+
});

src/test/datascience/jupyter/serverSelector.unit.test.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
import { assert } from 'chai';
4-
import { anything, instance, mock, when } from 'ts-mockito';
4+
import { anything, instance, mock, verify, when } from 'ts-mockito';
55

66
import { QuickPickItem } from 'vscode';
77
import { ApplicationShell } from '../../../client/common/application/applicationShell';
8+
import { CommandManager } from '../../../client/common/application/commandManager';
9+
import { ICommandManager } from '../../../client/common/application/types';
810
import { ConfigurationService } from '../../../client/common/configuration/service';
11+
import { IDataScienceSettings } from '../../../client/common/types';
912
import { DataScience } from '../../../client/common/utils/localize';
1013
import { noop } from '../../../client/common/utils/misc';
1114
import { MultiStepInputFactory } from '../../../client/common/utils/multiStepInput';
@@ -19,29 +22,38 @@ import { MockQuickPick } from '../mockQuickPick';
1922
// tslint:disable: max-func-body-length
2023
suite('Data Science - Jupyter Server URI Selector', () => {
2124
let quickPick: MockQuickPick | undefined;
22-
25+
let cmdManager: ICommandManager;
26+
let dsSettings: IDataScienceSettings;
2327
function createDataScienceObject(
2428
quickPickSelection: string,
2529
inputSelection: string,
2630
updateCallback: (val: string) => void,
2731
mockStorage?: MockMemento
2832
): JupyterServerSelector {
33+
dsSettings = {
34+
jupyterServerURI: Settings.JupyterServerLocalLaunch
35+
// tslint:disable-next-line: no-any
36+
} as any;
2937
const configService = mock(ConfigurationService);
3038
const applicationShell = mock(ApplicationShell);
39+
cmdManager = mock(CommandManager);
3140
const storage = mockStorage ? mockStorage : new MockMemento();
3241
quickPick = new MockQuickPick(quickPickSelection);
3342
const input = new MockInputBox(inputSelection);
43+
when(cmdManager.executeCommand(anything(), anything())).thenResolve();
3444
when(applicationShell.createQuickPick()).thenReturn(quickPick!);
3545
when(applicationShell.createInputBox()).thenReturn(input);
3646
const multiStepFactory = new MultiStepInputFactory(instance(applicationShell));
47+
// tslint:disable-next-line: no-any
48+
when(configService.getSettings(anything())).thenReturn({ datascience: dsSettings } as any);
3749
when(configService.updateSetting('dataScience.jupyterServerURI', anything(), anything(), anything())).thenCall(
3850
(_a1, a2, _a3, _a4) => {
3951
updateCallback(a2);
4052
return Promise.resolve();
4153
}
4254
);
4355

44-
return new JupyterServerSelector(storage, multiStepFactory, instance(configService));
56+
return new JupyterServerSelector(storage, multiStepFactory, instance(configService), instance(cmdManager));
4557
}
4658

4759
test('Local pick server uri', async () => {
@@ -130,6 +142,23 @@ suite('Data Science - Jupyter Server URI Selector', () => {
130142
assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value');
131143
});
132144

145+
test('Remote server uri (reload VSCode if there is a change in settings)', async () => {
146+
let value = '';
147+
const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', v => (value = v));
148+
await ds.selectJupyterURI();
149+
assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value');
150+
verify(cmdManager.executeCommand(anything(), anything())).once();
151+
});
152+
153+
test('Remote server uri (do not reload VSCode if there is no change in settings)', async () => {
154+
let value = '';
155+
const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', v => (value = v));
156+
dsSettings.jupyterServerURI = 'http://localhost:1111';
157+
await ds.selectJupyterURI();
158+
assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value');
159+
verify(cmdManager.executeCommand(anything(), anything())).never();
160+
});
161+
133162
test('Invalid server uri', async () => {
134163
let value = '';
135164
const ds = createDataScienceObject('$(server) Existing', 'httx://localhost:1111', v => (value = v));

0 commit comments

Comments
 (0)