diff --git a/news/1 Enhancements/9163.md b/news/1 Enhancements/9163.md new file mode 100644 index 000000000000..c8685481e8d5 --- /dev/null +++ b/news/1 Enhancements/9163.md @@ -0,0 +1 @@ +When entering remote Jupyter Server, default the input value to uri in clipboard. diff --git a/src/client/common/application/clipboard.ts b/src/client/common/application/clipboard.ts new file mode 100644 index 000000000000..619d9ea60b1e --- /dev/null +++ b/src/client/common/application/clipboard.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { env } from 'vscode'; +import { IClipboard } from './types'; + +@injectable() +export class ClipboardService implements IClipboard { + public async readText(): Promise { + return env.clipboard.readText(); + } + public async writeText(value: string): Promise { + await env.clipboard.writeText(value); + } +} diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 68f39500ccc1..d512afbf0728 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -1197,3 +1197,16 @@ export interface ICustomEditorService { */ openEditor(file: Uri): Promise; } + +export const IClipboard = Symbol('IClipboard'); +export interface IClipboard { + /** + * Read the current clipboard contents as text. + */ + readText(): Promise; + + /** + * Writes text into the clipboard. + */ + writeText(value: string): Promise; +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 876e3b4d4eb0..a0b1d785f566 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -9,6 +9,7 @@ import { ImportTracker } from '../telemetry/importTracker'; import { IImportTracker } from '../telemetry/types'; import { ApplicationEnvironment } from './application/applicationEnvironment'; import { ApplicationShell } from './application/applicationShell'; +import { ClipboardService } from './application/clipboard'; import { CommandManager } from './application/commandManager'; import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; import { CustomEditorService } from './application/customEditorService'; @@ -21,6 +22,7 @@ import { TerminalManager } from './application/terminalManager'; import { IApplicationEnvironment, IApplicationShell, + IClipboard, ICommandManager, ICustomEditorService, IDebugService, @@ -113,6 +115,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(IApplicationShell, ApplicationShell); + serviceManager.addSingleton(IClipboard, ClipboardService); serviceManager.addSingleton(ICurrentProcess, CurrentProcess); serviceManager.addSingleton(IInstaller, ProductInstaller); serviceManager.addSingleton(ICommandManager, CommandManager); diff --git a/src/client/datascience/jupyter/serverSelector.ts b/src/client/datascience/jupyter/serverSelector.ts index 24d68115648d..bfade9c79cd5 100644 --- a/src/client/datascience/jupyter/serverSelector.ts +++ b/src/client/datascience/jupyter/serverSelector.ts @@ -4,8 +4,8 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import { ConfigurationTarget, Memento, QuickPickItem } from 'vscode'; -import { ICommandManager } from '../../common/application/types'; +import { ConfigurationTarget, Memento, QuickPickItem, Uri } from 'vscode'; +import { IClipboard, ICommandManager } from '../../common/application/types'; import { GLOBAL_MEMENTO, IConfigurationService, IMemento } from '../../common/types'; import { DataScience } from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; @@ -19,6 +19,8 @@ import { captureTelemetry } from '../../telemetry'; import { getSavedUriList } from '../common'; import { Settings, Telemetry } from '../constants'; +const defaultUri = 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe'; + interface ISelectUriQuickPickItem extends QuickPickItem { newChoice: boolean; } @@ -29,6 +31,7 @@ export class JupyterServerSelector { private readonly newLabel = `$(server) ${DataScience.jupyterSelectURINewLabel()}`; constructor( @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IClipboard) private readonly clipboard: IClipboard, @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, @inject(IConfigurationService) private configuration: IConfigurationService, @inject(ICommandManager) private cmdManager: ICommandManager @@ -57,10 +60,19 @@ export class JupyterServerSelector { } } private async selectRemoteURI(input: IMultiStepInput<{}>, _state: {}): Promise | void> { + let initialValue = defaultUri; + try { + const text = await this.clipboard.readText().catch(() => ''); + const parsedUri = Uri.parse(text.trim(), true); + // Only display http/https uris. + initialValue = text && parsedUri && parsedUri.scheme.toLowerCase().startsWith('http') ? text : defaultUri; + } catch { + // We can ignore errors. + } // Ask the user to enter a URI to connect to. const uri = await input.showInputBox({ title: DataScience.jupyterSelectURIPrompt(), - value: 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe', + value: initialValue || defaultUri, validate: this.validateSelectJupyterURI, prompt: '' }); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 09f515ea796f..0abc1dc75b71 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -64,9 +64,11 @@ import { IDiagnosticHandlerService, IDiagnosticsService } from '../../client/application/diagnostics/types'; +import { ClipboardService } from '../../client/common/application/clipboard'; import { TerminalManager } from '../../client/common/application/terminalManager'; import { IApplicationShell, + IClipboard, ICommandManager, ICustomEditorService, IDebugService, @@ -861,6 +863,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { ); this.serviceManager.addSingletonInstance(IApplicationShell, appShell.object); + this.serviceManager.addSingleton(IClipboard, ClipboardService); this.serviceManager.addSingletonInstance(IDocumentManager, this.documentManager); this.serviceManager.addSingletonInstance(IWorkspaceService, instance(workspaceService)); this.serviceManager.addSingletonInstance( diff --git a/src/test/datascience/jupyter/serverSelector.unit.test.ts b/src/test/datascience/jupyter/serverSelector.unit.test.ts index 3bdca78921cc..526700658842 100644 --- a/src/test/datascience/jupyter/serverSelector.unit.test.ts +++ b/src/test/datascience/jupyter/serverSelector.unit.test.ts @@ -3,15 +3,17 @@ import { assert } from 'chai'; import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import { QuickPickItem } from 'vscode'; import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { ClipboardService } from '../../../client/common/application/clipboard'; import { CommandManager } from '../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../client/common/application/types'; +import { IClipboard, ICommandManager } from '../../../client/common/application/types'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { IDataScienceSettings } from '../../../client/common/types'; import { DataScience } from '../../../client/common/utils/localize'; import { noop } from '../../../client/common/utils/misc'; -import { MultiStepInputFactory } from '../../../client/common/utils/multiStepInput'; +import { MultiStepInput, MultiStepInputFactory } from '../../../client/common/utils/multiStepInput'; import { addToUriList } from '../../../client/datascience/common'; import { Settings } from '../../../client/datascience/constants'; import { JupyterServerSelector } from '../../../client/datascience/jupyter/serverSelector'; @@ -24,6 +26,8 @@ suite('Data Science - Jupyter Server URI Selector', () => { let quickPick: MockQuickPick | undefined; let cmdManager: ICommandManager; let dsSettings: IDataScienceSettings; + let clipboard: IClipboard; + function createDataScienceObject( quickPickSelection: string, inputSelection: string, @@ -34,6 +38,7 @@ suite('Data Science - Jupyter Server URI Selector', () => { jupyterServerURI: Settings.JupyterServerLocalLaunch // tslint:disable-next-line: no-any } as any; + clipboard = mock(ClipboardService); const configService = mock(ConfigurationService); const applicationShell = mock(ApplicationShell); cmdManager = mock(CommandManager); @@ -53,9 +58,17 @@ suite('Data Science - Jupyter Server URI Selector', () => { } ); - return new JupyterServerSelector(storage, multiStepFactory, instance(configService), instance(cmdManager)); + return new JupyterServerSelector( + storage, + instance(clipboard), + multiStepFactory, + instance(configService), + instance(cmdManager) + ); } + teardown(() => sinon.restore()); + test('Local pick server uri', async () => { let value = ''; const ds = createDataScienceObject('$(zap) Default', '', v => (value = v)); @@ -166,4 +179,36 @@ suite('Data Science - Jupyter Server URI Selector', () => { assert.notEqual(value, 'httx://localhost:1111', 'Already running should validate'); assert.equal(value, '', 'Validation failed'); }); + + suite('Default Uri when selecting remote uri', () => { + const defaultUri = 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe'; + + async function testDefaultUri(expectedDefaultUri: string, clipboardValue?: string) { + const showInputBox = sinon.spy(MultiStepInput.prototype, 'showInputBox'); + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', noop); + when(clipboard.readText()).thenResolve(clipboardValue || ''); + + await ds.selectJupyterURI(); + + assert.equal(showInputBox.firstCall.args[0].value, expectedDefaultUri); + } + + test('Display default uri', async () => { + await testDefaultUri(defaultUri); + }); + test('Display default uri if clipboard is empty', async () => { + await testDefaultUri(defaultUri, ''); + }); + test('Display default uri if clipboard contains invalid uri, display default uri', async () => { + await testDefaultUri(defaultUri, 'Hello World!'); + }); + test('Display default uri if clipboard contains invalid file uri, display default uri', async () => { + await testDefaultUri(defaultUri, 'file://test.pdf'); + }); + test('Display default uri if clipboard contains a valid uri, display uri from clipboard', async () => { + const validUri = 'https://wow:0909/?password=1234'; + + await testDefaultUri(validUri, validUri); + }); + }); }); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 0712ce3810c6..48b7c0adf495 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -23,6 +23,15 @@ function generateMock(name: K): void { mockedVSCodeNamespaces[name] = mockedObj as any; } +class MockClipboard { + private text: string = ''; + public readText(): Promise { + return Promise.resolve(this.text); + } + public async writeText(value: string): Promise { + this.text = value; + } +} export function initialize() { generateMock('workspace'); generateMock('window'); @@ -32,6 +41,10 @@ export function initialize() { generateMock('debug'); generateMock('scm'); + // Use mock clipboard fo testing purposes. + const clipboard = new MockClipboard(); + mockedVSCodeNamespaces.env?.setup(e => e.clipboard).returns(() => clipboard); + // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). Module._load = function(request: any, _parent: any) { if (request === 'vscode') {