Skip to content

Commit

Permalink
Updates to API
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne committed Aug 15, 2023
1 parent d844fef commit 4a0cbbe
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 117 deletions.
2 changes: 1 addition & 1 deletion src/api.internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ declare module './api' {
* Returns a list of commands to be displayed to the user.
* @param value The value entered by the user in the quick pick.
*/
getCommands(token: CancellationToken, value: string): Promise<Command[]>;
getCommands(value: string, token: CancellationToken): Promise<JupyterServerCommand[]>;
}

export interface IJupyterUriProvider {
Expand Down
59 changes: 30 additions & 29 deletions src/api.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ declare module './api' {
*/
readonly token?: string;
/**
* Authorization header to be used when connecting to the server.
* HTTP header to be used when connecting to the server.
*/
readonly authorizationHeader?: Record<string, string>;
readonly headers?: Record<string, string>;
/**
* The local directory that maps to the remote directory of the Jupyter Server.
* E.g. assume you start Jupyter Notebook with --notebook-dir=/foo/bar,
Expand Down Expand Up @@ -62,10 +62,6 @@ declare module './api' {
* A human-readable string representing the name of the Server. This can be read and updated by the extension.
*/
label: string;
/**
* Returns the connection information for this server.
*/
resolveConnectionInformation(token: CancellationToken): Promise<JupyterServerConnectionInformation>;
}

/**
Expand All @@ -81,12 +77,16 @@ declare module './api' {
* Returns the list of servers.
*/
getJupyterServers(token: CancellationToken): Promise<JupyterServer[]>;
/**
* Returns the connection information for the Jupyter server.
*/
resolveConnectionInformation(
server: JupyterServer,
token: CancellationToken
): Promise<JupyterServerConnectionInformation>;
}
/**
* Represents a reference to a Jupyter Server command. Provides a title which
* will be used to represent a command in the UI and, optionally,
* an array of arguments which will be passed to the command handler
* function when invoked.
* Represents a reference to a Jupyter Server command.
*/
export interface JupyterServerCommand {
/**
Expand All @@ -97,40 +97,37 @@ declare module './api' {
* A human-readable string which is rendered less prominent in a separate line.
*/
detail?: string;
/**
* The identifier of the actual command handler.
* @see {@link commands.registerCommand}
*/
command: string;
/**
* A tooltip for the command, when represented in the UI.
*/
tooltip?: string;
/**
* Arguments that the command handler should be
* invoked with.
* Default command to be used when there are no servers.
* If not set, and there are not servers, then the user will be prompted to select a command from a list of commands returned by `getCommands`.
*/
arguments?: unknown[];
picked?: boolean;
}
/**
* Provider of Jupyter Server Commands.
* Each command allows the user to perform an action.
* The return value of the command should be of the form Promise<JupyterServer | 'back' | undefined>
* The returned value have the following meaning:
* - JupyterServer : The Jupyter Server object that was created
* - 'back' : Go back to the previous screen
* - undefined|void : Do nothing
*/
export interface JupyterServerCommandProvider {
/**
* Default command to be used when there are no servers. This can be read and updated by the extension.
* If not set, and there are not servers, then the user will be prompted to select a command from a list of commands returned by `getCommands`.
* Returns a list of commands to be displayed to the user.
* If there are no JupyterServers and one of the returned commands has `picked = true`,
* then the `handleCommand` method will be invoked with that command.
* @param options Reserved for future use. Extensions can ignore this argument for now.
*/
selected?: JupyterServerCommand;
getCommands(options: unknown, token: CancellationToken): Promise<JupyterServerCommand[]>;
/**
* Returns a list of commands to be displayed to the user.
* Invoked when a command has been selected.
* @param command The command selected by the user.
* @returns
* - JupyterServer : The Jupyter Server object that was created
* - 'back' : Go back to the previous UI in the workflow
* - undefined|void : Do nothing
*/
getCommands(token: CancellationToken): Promise<JupyterServerCommand[]>;
handleCommand(command: JupyterServerCommand): Promise<JupyterServer | 'back' | undefined | void>;
}
export interface JupyterServerCollection {
/**
Expand Down Expand Up @@ -161,7 +158,11 @@ declare module './api' {
export interface JupyterAPI {
/**
* Creates a Jupyter Server Collection that can be displayed in the Notebook Kernel Picker.
*
* The ideal time to invoke this method would be when a Notebook Document has been opened.
* Calling this during activation of the extension might not be ideal, as this would result in
* unnecessarily activating the Jupyter extension as well.
*/
createJupyterServerCollection(id: string, label: string): Promise<JupyterServerCollection>;
createJupyterServerCollection(id: string, label: string): JupyterServerCollection;
}
}
4 changes: 3 additions & 1 deletion src/extension.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ export async function activate(context: IExtensionContext): Promise<IExtensionAp
getSuggestedController: () => Promise.resolve(undefined),
addRemoteJupyterServer: () => Promise.resolve(undefined),
openNotebook: () => Promise.reject(),
createJupyterServerCollection: () => Promise.reject()
createJupyterServerCollection: () => {
throw new Error('Not Implemented');
}
};
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/extension.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ export async function activate(context: IExtensionContext): Promise<IExtensionAp
getSuggestedController: () => Promise.resolve(undefined),
addRemoteJupyterServer: () => Promise.resolve(undefined),
openNotebook: () => Promise.reject(),
createJupyterServerCollection: () => Promise.reject()
createJupyterServerCollection: () => {
throw new Error('Not Implemented')
}
};
}
}
Expand Down
34 changes: 14 additions & 20 deletions src/kernels/jupyter/connection/jupyterServerProviderRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
CancellationToken,
CancellationTokenSource,
Command,
EventEmitter,
QuickPickItem,
Uri,
commands
} from 'vscode';
import { CancellationToken, CancellationTokenSource, EventEmitter, QuickPickItem, Uri } from 'vscode';
import {
IJupyterServerUri,
IJupyterUriProvider,
JupyterServer,
JupyterServerCollection,
JupyterServerCommand,
JupyterServerCommandProvider,
JupyterServerProvider
} from '../../../api';
Expand Down Expand Up @@ -105,27 +98,26 @@ class JupyterUriProviderAdaptor extends Disposables implements IJupyterUriProvid
);
}
}
private commands = new Map<string, Command>();
private commands = new Map<string, JupyterServerCommand>();
async getQuickPickEntryItems(value?: string): Promise<(QuickPickItem & { default?: boolean | undefined })[]> {
if (!this.provider.commandProvider) {
throw new Error(`No Jupyter Server Command Provider for ${this.provider.extensionId}#${this.provider.id}`);
}
const token = new CancellationTokenSource();
try {
value = this.provider.extensionId === JVSC_EXTENSION_ID ? value : undefined;
const items = await this.provider.commandProvider.getCommands(token.token, value || '');
const items = await this.provider.commandProvider.getCommands(value || '', token.token);
if (this.provider.extensionId === JVSC_EXTENSION_ID) {
if (!value) {
this.commands.clear();
}
items.forEach((c) => this.commands.set(c.title, c));
}
const selectedCommand = items.find((c) => c.title === this.provider.commandProvider?.selected?.title);
return items.map((c) => {
return {
label: c.title,
tooltip: c.tooltip,
default: c === selectedCommand
default: c.picked === true
};
});
} catch (ex) {
Expand All @@ -144,18 +136,15 @@ class JupyterUriProviderAdaptor extends Disposables implements IJupyterUriProvid
}
const token = new CancellationTokenSource();
try {
const items = await this.provider.commandProvider.getCommands(token.token);
const items = await this.provider.commandProvider.getCommands('', token.token);
const command = items.find((c) => c.title === item.label) || this.commands.get(item.label);
if (!command) {
throw new Error(
`Jupyter Server Command ${item.label} not found in Command Provider ${this.provider.extensionId}#${this.provider.id}`
);
}
try {
const result: JupyterServer | 'back' | undefined = await commands.executeCommand(
command.command,
...(command.arguments || [])
);
const result = await this.provider.commandProvider.handleCommand(command);
if (result === 'back') {
return result;
}
Expand All @@ -172,14 +161,19 @@ class JupyterUriProviderAdaptor extends Disposables implements IJupyterUriProvid
}
async getServerUri(handle: string): Promise<IJupyterServerUri> {
const token = new CancellationTokenSource();
if (!this.provider.serverProvider) {
throw new Error(
`Server Provider not initialized, Extension: ${this.extensionId}:${this.id}, Server ${handle}`
);
}
try {
const server = await this.getServer(handle, token.token);
const info = await server.resolveConnectionInformation(token.token);
const info = await this.provider.serverProvider?.resolveConnectionInformation(server, token.token);
return {
baseUrl: info.baseUrl.toString(),
displayName: server.label,
token: info.token || '',
authorizationHeader: info.authorizationHeader,
authorizationHeader: info.headers,
mappedRemoteNotebookDir: info.mappedRemoteNotebookDir?.toString(),
webSocketProtocols: info.webSocketProtocols
};
Expand Down
111 changes: 90 additions & 21 deletions src/standalone/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ import { IControllerRegistration } from '../../notebooks/controllers/types';
import { sendTelemetryEvent } from '../../telemetry';
import { noop } from '../../platform/common/utils/misc';
import { isRemoteConnection } from '../../kernels/types';
import { JupyterAPI, IExportedKernelService, IJupyterUriProvider } from '../../api';
import { createPublicAPIProxy } from '../../platform/common/helpers';
import { Disposables } from '../../platform/common/utils';
import {
JupyterAPI,
IExportedKernelService,
IJupyterUriProvider,
JupyterServerCollection,
JupyterServerProvider,
JupyterServerCommandProvider
} from '../../api';

export const IExportedKernelServiceFactory = Symbol('IExportedKernelServiceFactory');
export interface IExportedKernelServiceFactory {
Expand Down Expand Up @@ -157,24 +162,88 @@ export function buildApi(
});
return notebookEditor.notebook;
},
createJupyterServerCollection: async (id, label) => {
sendApiUsageTelemetry(extensions, 'createJupyterServerCollection');
const extensionId = (await extensions.determineExtensionFromCallStack()).extensionId;
if (
![JVSC_EXTENSION_ID.split('.')[0], 'SynapseVSCode', 'GitHub']
.map((s) => s.toLowerCase())
.includes(extensionId.toLowerCase())
) {
throw new Error(`Access to Proposed API not allowed, as it is subject to change.`);
}
const registration = serviceContainer.get<IJupyterServerProviderRegistry>(IJupyterServerProviderRegistry);
const collection = registration.createJupyterServerCollection(extensionId, id, label);
// Do not expose unwanted properties to the extensions.
return createPublicAPIProxy(collection as unknown as typeof collection & Disposables, [
'disposables',
'isDisposed',
'dispose'
]);
createJupyterServerCollection: (id, label) => {
let documentation: Uri | undefined;
let serverProvider: JupyterServerProvider | undefined;
let commandProvider: JupyterServerCommandProvider | undefined;
let disposeHandler = noop;
let isDisposed = false;
let proxy: JupyterServerCollection | undefined;
const collection: JupyterServerCollection = {
dispose: () => {
isDisposed = true;
disposeHandler();
},
extensionId: '',
get id() {
return id;
},
set label(value: string) {
label = value;
if (proxy) {
proxy.label = value;
}
},
get label() {
return label;
},
set documentation(value: Uri | undefined) {
documentation = value;
if (proxy) {
proxy.documentation = value;
}
},
get documentation() {
return documentation;
},
set commandProvider(value: JupyterServerCommandProvider | undefined) {
commandProvider = value;
if (proxy) {
proxy.commandProvider = value;
}
},
get commandProvider() {
return commandProvider;
},
set serverProvider(value: JupyterServerProvider | undefined) {
serverProvider = value;
if (proxy) {
proxy.serverProvider = value;
}
},
get serverProvider() {
return serverProvider;
}
};
let extensionId = '';
(async () => {
sendApiUsageTelemetry(extensions, 'createJupyterServerCollection');
extensionId = (await extensions.determineExtensionFromCallStack()).extensionId;
if (
![JVSC_EXTENSION_ID.split('.')[0], 'SynapseVSCode', 'GitHub']
.map((s) => s.toLowerCase())
.includes(extensionId.toLowerCase())
) {
throw new Error(`Access to Proposed API not allowed, as it is subject to change.`);
}
const registration =
serviceContainer.get<IJupyterServerProviderRegistry>(IJupyterServerProviderRegistry);
const proxy = registration.createJupyterServerCollection(extensionId, id, label);
proxy.label = label;
proxy.documentation = documentation;
proxy.commandProvider = commandProvider;
proxy.serverProvider = serverProvider;
if (isDisposed) {
proxy.dispose();
}
disposeHandler = proxy.dispose.bind(proxy);
})().catch((ex) =>
traceError(
`Failed to create Jupyter Server Collection for ${id}:${label} & extension ${extensionId}`,
ex
)
);
return collection;
}
};

Expand Down
Loading

0 comments on commit 4a0cbbe

Please sign in to comment.