Skip to content

Commit 5ed5103

Browse files
authored
Default uri in input to value in clipboard (#10413)
* Default uri in input to value in clipboard * Remove empty space * Fix linter
1 parent f61d3a6 commit 5ed5103

8 files changed

Lines changed: 114 additions & 6 deletions

File tree

news/1 Enhancements/9163.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When entering remote Jupyter Server, default the input value to uri in clipboard.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { injectable } from 'inversify';
7+
import { env } from 'vscode';
8+
import { IClipboard } from './types';
9+
10+
@injectable()
11+
export class ClipboardService implements IClipboard {
12+
public async readText(): Promise<string> {
13+
return env.clipboard.readText();
14+
}
15+
public async writeText(value: string): Promise<void> {
16+
await env.clipboard.writeText(value);
17+
}
18+
}

src/client/common/application/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,3 +1197,16 @@ export interface ICustomEditorService {
11971197
*/
11981198
openEditor(file: Uri): Promise<void>;
11991199
}
1200+
1201+
export const IClipboard = Symbol('IClipboard');
1202+
export interface IClipboard {
1203+
/**
1204+
* Read the current clipboard contents as text.
1205+
*/
1206+
readText(): Promise<string>;
1207+
1208+
/**
1209+
* Writes text into the clipboard.
1210+
*/
1211+
writeText(value: string): Promise<void>;
1212+
}

src/client/common/serviceRegistry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ImportTracker } from '../telemetry/importTracker';
99
import { IImportTracker } from '../telemetry/types';
1010
import { ApplicationEnvironment } from './application/applicationEnvironment';
1111
import { ApplicationShell } from './application/applicationShell';
12+
import { ClipboardService } from './application/clipboard';
1213
import { CommandManager } from './application/commandManager';
1314
import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand';
1415
import { CustomEditorService } from './application/customEditorService';
@@ -21,6 +22,7 @@ import { TerminalManager } from './application/terminalManager';
2122
import {
2223
IApplicationEnvironment,
2324
IApplicationShell,
25+
IClipboard,
2426
ICommandManager,
2527
ICustomEditorService,
2628
IDebugService,
@@ -113,6 +115,7 @@ export function registerTypes(serviceManager: IServiceManager) {
113115
serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory);
114116
serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils);
115117
serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell);
118+
serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService);
116119
serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
117120
serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller);
118121
serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager);

src/client/datascience/jupyter/serverSelector.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
'use strict';
55

66
import { inject, injectable, named } from 'inversify';
7-
import { ConfigurationTarget, Memento, QuickPickItem } from 'vscode';
8-
import { ICommandManager } from '../../common/application/types';
7+
import { ConfigurationTarget, Memento, QuickPickItem, Uri } from 'vscode';
8+
import { IClipboard, ICommandManager } from '../../common/application/types';
99
import { GLOBAL_MEMENTO, IConfigurationService, IMemento } from '../../common/types';
1010
import { DataScience } from '../../common/utils/localize';
1111
import { noop } from '../../common/utils/misc';
@@ -19,6 +19,8 @@ import { captureTelemetry } from '../../telemetry';
1919
import { getSavedUriList } from '../common';
2020
import { Settings, Telemetry } from '../constants';
2121

22+
const defaultUri = 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe';
23+
2224
interface ISelectUriQuickPickItem extends QuickPickItem {
2325
newChoice: boolean;
2426
}
@@ -29,6 +31,7 @@ export class JupyterServerSelector {
2931
private readonly newLabel = `$(server) ${DataScience.jupyterSelectURINewLabel()}`;
3032
constructor(
3133
@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento,
34+
@inject(IClipboard) private readonly clipboard: IClipboard,
3235
@inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory,
3336
@inject(IConfigurationService) private configuration: IConfigurationService,
3437
@inject(ICommandManager) private cmdManager: ICommandManager
@@ -57,10 +60,19 @@ export class JupyterServerSelector {
5760
}
5861
}
5962
private async selectRemoteURI(input: IMultiStepInput<{}>, _state: {}): Promise<InputStep<{}> | void> {
63+
let initialValue = defaultUri;
64+
try {
65+
const text = await this.clipboard.readText().catch(() => '');
66+
const parsedUri = Uri.parse(text.trim(), true);
67+
// Only display http/https uris.
68+
initialValue = text && parsedUri && parsedUri.scheme.toLowerCase().startsWith('http') ? text : defaultUri;
69+
} catch {
70+
// We can ignore errors.
71+
}
6072
// Ask the user to enter a URI to connect to.
6173
const uri = await input.showInputBox({
6274
title: DataScience.jupyterSelectURIPrompt(),
63-
value: 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe',
75+
value: initialValue || defaultUri,
6476
validate: this.validateSelectJupyterURI,
6577
prompt: ''
6678
});

src/test/datascience/dataScienceIocContainer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ import {
6464
IDiagnosticHandlerService,
6565
IDiagnosticsService
6666
} from '../../client/application/diagnostics/types';
67+
import { ClipboardService } from '../../client/common/application/clipboard';
6768
import { TerminalManager } from '../../client/common/application/terminalManager';
6869
import {
6970
IApplicationShell,
71+
IClipboard,
7072
ICommandManager,
7173
ICustomEditorService,
7274
IDebugService,
@@ -861,6 +863,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
861863
);
862864

863865
this.serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object);
866+
this.serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService);
864867
this.serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, this.documentManager);
865868
this.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, instance(workspaceService));
866869
this.serviceManager.addSingletonInstance<IConfigurationService>(

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

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
import { assert } from 'chai';
44
import { anything, instance, mock, verify, when } from 'ts-mockito';
55

6+
import * as sinon from 'sinon';
67
import { QuickPickItem } from 'vscode';
78
import { ApplicationShell } from '../../../client/common/application/applicationShell';
9+
import { ClipboardService } from '../../../client/common/application/clipboard';
810
import { CommandManager } from '../../../client/common/application/commandManager';
9-
import { ICommandManager } from '../../../client/common/application/types';
11+
import { IClipboard, ICommandManager } from '../../../client/common/application/types';
1012
import { ConfigurationService } from '../../../client/common/configuration/service';
1113
import { IDataScienceSettings } from '../../../client/common/types';
1214
import { DataScience } from '../../../client/common/utils/localize';
1315
import { noop } from '../../../client/common/utils/misc';
14-
import { MultiStepInputFactory } from '../../../client/common/utils/multiStepInput';
16+
import { MultiStepInput, MultiStepInputFactory } from '../../../client/common/utils/multiStepInput';
1517
import { addToUriList } from '../../../client/datascience/common';
1618
import { Settings } from '../../../client/datascience/constants';
1719
import { JupyterServerSelector } from '../../../client/datascience/jupyter/serverSelector';
@@ -24,6 +26,8 @@ suite('Data Science - Jupyter Server URI Selector', () => {
2426
let quickPick: MockQuickPick | undefined;
2527
let cmdManager: ICommandManager;
2628
let dsSettings: IDataScienceSettings;
29+
let clipboard: IClipboard;
30+
2731
function createDataScienceObject(
2832
quickPickSelection: string,
2933
inputSelection: string,
@@ -34,6 +38,7 @@ suite('Data Science - Jupyter Server URI Selector', () => {
3438
jupyterServerURI: Settings.JupyterServerLocalLaunch
3539
// tslint:disable-next-line: no-any
3640
} as any;
41+
clipboard = mock(ClipboardService);
3742
const configService = mock(ConfigurationService);
3843
const applicationShell = mock(ApplicationShell);
3944
cmdManager = mock(CommandManager);
@@ -53,9 +58,17 @@ suite('Data Science - Jupyter Server URI Selector', () => {
5358
}
5459
);
5560

56-
return new JupyterServerSelector(storage, multiStepFactory, instance(configService), instance(cmdManager));
61+
return new JupyterServerSelector(
62+
storage,
63+
instance(clipboard),
64+
multiStepFactory,
65+
instance(configService),
66+
instance(cmdManager)
67+
);
5768
}
5869

70+
teardown(() => sinon.restore());
71+
5972
test('Local pick server uri', async () => {
6073
let value = '';
6174
const ds = createDataScienceObject('$(zap) Default', '', v => (value = v));
@@ -166,4 +179,36 @@ suite('Data Science - Jupyter Server URI Selector', () => {
166179
assert.notEqual(value, 'httx://localhost:1111', 'Already running should validate');
167180
assert.equal(value, '', 'Validation failed');
168181
});
182+
183+
suite('Default Uri when selecting remote uri', () => {
184+
const defaultUri = 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe';
185+
186+
async function testDefaultUri(expectedDefaultUri: string, clipboardValue?: string) {
187+
const showInputBox = sinon.spy(MultiStepInput.prototype, 'showInputBox');
188+
const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', noop);
189+
when(clipboard.readText()).thenResolve(clipboardValue || '');
190+
191+
await ds.selectJupyterURI();
192+
193+
assert.equal(showInputBox.firstCall.args[0].value, expectedDefaultUri);
194+
}
195+
196+
test('Display default uri', async () => {
197+
await testDefaultUri(defaultUri);
198+
});
199+
test('Display default uri if clipboard is empty', async () => {
200+
await testDefaultUri(defaultUri, '');
201+
});
202+
test('Display default uri if clipboard contains invalid uri, display default uri', async () => {
203+
await testDefaultUri(defaultUri, 'Hello World!');
204+
});
205+
test('Display default uri if clipboard contains invalid file uri, display default uri', async () => {
206+
await testDefaultUri(defaultUri, 'file://test.pdf');
207+
});
208+
test('Display default uri if clipboard contains a valid uri, display uri from clipboard', async () => {
209+
const validUri = 'https://wow:0909/?password=1234';
210+
211+
await testDefaultUri(validUri, validUri);
212+
});
213+
});
169214
});

src/test/vscode-mock.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ function generateMock<K extends keyof VSCode>(name: K): void {
2323
mockedVSCodeNamespaces[name] = mockedObj as any;
2424
}
2525

26+
class MockClipboard {
27+
private text: string = '';
28+
public readText(): Promise<string> {
29+
return Promise.resolve(this.text);
30+
}
31+
public async writeText(value: string): Promise<void> {
32+
this.text = value;
33+
}
34+
}
2635
export function initialize() {
2736
generateMock('workspace');
2837
generateMock('window');
@@ -32,6 +41,10 @@ export function initialize() {
3241
generateMock('debug');
3342
generateMock('scm');
3443

44+
// Use mock clipboard fo testing purposes.
45+
const clipboard = new MockClipboard();
46+
mockedVSCodeNamespaces.env?.setup(e => e.clipboard).returns(() => clipboard);
47+
3548
// When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports).
3649
Module._load = function(request: any, _parent: any) {
3750
if (request === 'vscode') {

0 commit comments

Comments
 (0)