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
1 change: 1 addition & 0 deletions news/1 Enhancements/9163.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When entering remote Jupyter Server, default the input value to uri in clipboard.
18 changes: 18 additions & 0 deletions src/client/common/application/clipboard.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return env.clipboard.readText();
}
public async writeText(value: string): Promise<void> {
await env.clipboard.writeText(value);
}
}
13 changes: 13 additions & 0 deletions src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,3 +1197,16 @@ export interface ICustomEditorService {
*/
openEditor(file: Uri): Promise<void>;
}

export const IClipboard = Symbol('IClipboard');
export interface IClipboard {
/**
* Read the current clipboard contents as text.
*/
readText(): Promise<string>;

/**
* Writes text into the clipboard.
*/
writeText(value: string): Promise<void>;
}
3 changes: 3 additions & 0 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,6 +22,7 @@ import { TerminalManager } from './application/terminalManager';
import {
IApplicationEnvironment,
IApplicationShell,
IClipboard,
ICommandManager,
ICustomEditorService,
IDebugService,
Expand Down Expand Up @@ -113,6 +115,7 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory);
serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils);
serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell);
serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService);
serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller);
serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager);
Expand Down
18 changes: 15 additions & 3 deletions src/client/datascience/jupyter/serverSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -57,10 +60,19 @@ export class JupyterServerSelector {
}
}
private async selectRemoteURI(input: IMultiStepInput<{}>, _state: {}): Promise<InputStep<{}> | 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: ''
});
Expand Down
3 changes: 3 additions & 0 deletions src/test/datascience/dataScienceIocContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -861,6 +863,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
);

this.serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object);
this.serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService);
this.serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, this.documentManager);
this.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, instance(workspaceService));
this.serviceManager.addSingletonInstance<IConfigurationService>(
Expand Down
51 changes: 48 additions & 3 deletions src/test/datascience/jupyter/serverSelector.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -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);
});
});
});
13 changes: 13 additions & 0 deletions src/test/vscode-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ function generateMock<K extends keyof VSCode>(name: K): void {
mockedVSCodeNamespaces[name] = mockedObj as any;
}

class MockClipboard {
private text: string = '';
public readText(): Promise<string> {
return Promise.resolve(this.text);
}
public async writeText(value: string): Promise<void> {
this.text = value;
}
}
export function initialize() {
generateMock('workspace');
generateMock('window');
Expand All @@ -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') {
Expand Down