Skip to content

Commit

Permalink
Display the connections pane
Browse files Browse the repository at this point in the history
The connections pane is now displayed when a
connection is openend from the variables pane,
or using `%connection_show` line magic.
  • Loading branch information
dfalbel committed Apr 10, 2024
1 parent c2be2f3 commit ae36eab
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 9 deletions.
98 changes: 91 additions & 7 deletions extensions/positron-connections/src/comms/BaseComm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// to the backend as well as the logic to handle errors returned by the backend.

import * as positron from 'positron';
import { Disposable, Event } from 'vscode';
import { randomUUID } from 'crypto';

/**
* An enum representing the set of JSON-RPC error codes.
Expand All @@ -18,7 +20,7 @@ export enum JsonRpcErrorCode {
InvalidParams = -32602,
InternalError = -32603,
ServerErrorStart = -32000,
ServerErrorEnd = -32099
ServerErrorEnd = -32099,
}

/**
Expand All @@ -41,6 +43,51 @@ export interface PositronCommError {
data: any | undefined;
}

/**
* An event emitter that can be used to fire events from the backend to the
* frontend.
*/
class PositronCommEmitter<T> {
private _event?: Event<T>;
private _listeners: Record<string, (data: T) => void> = {};
/**
* Create a new event emitter.
*
* @param name The name of the event, as a JSON-RPC method name.
* @param properties The names of the properties in the event payload; used
* to convert positional parameters to named parameters.
*/
constructor(readonly name: string, readonly properties: string[]) {
this.name = name;
this.properties = properties;
}

get event(): Event<T> {
if (!this._event) {
this._event = (listener: (data: T) => void, thisArgs?, disposables?) => {
if (disposables) {
throw new Error('Disposables are not supported');
}

if (thisArgs) {
throw new Error('thisArgs is not supported');
}

const uuid = randomUUID();
this._listeners[uuid] = listener;
return new Disposable(() => {
delete this._listeners[uuid];
});
};
}
return this._event;
}

fire(data: T) {
Object.values(this._listeners).map((listener) => listener(data));
}
}

/**
* A base class for Positron comm instances. This class handles communication
* with the backend, and provides methods for creating event emitters and
Expand All @@ -49,14 +96,15 @@ export interface PositronCommError {
* Used by generated comm classes.
*/
export class PositronBaseComm {

/**
* Create a new Positron com
*
* @param clientInstance The client instance to use for communication with the backend.
* This instance must be connected to the backend before it is passed to this class.
*/
constructor(private readonly clientInstance: positron.RuntimeClientInstance) { }
constructor(
private readonly clientInstance: positron.RuntimeClientInstance
) { }

/**
* Perform an RPC and wait for the result.
Expand All @@ -67,10 +115,11 @@ export class PositronBaseComm {
* @returns A promise that resolves to the result of the RPC, or rejects
* with a PositronCommError.
*/
protected async performRpc<T>(rpcName: string,
protected async performRpc<T>(
rpcName: string,
paramNames: Array<string>,
paramValues: Array<any>): Promise<T> {

paramValues: Array<any>
): Promise<T> {
// Create the RPC arguments from the parameter names and values. This
// allows us to pass the parameters as positional parameters, but
// still have them be named parameters in the RPC.
Expand Down Expand Up @@ -125,7 +174,8 @@ export class PositronBaseComm {
if (!Object.keys(response).includes('result')) {
const error: PositronCommError = {
code: JsonRpcErrorCode.InternalError,
message: `Invalid response from ${this.clientInstance.getClientId()}: ` +
message:
`Invalid response from ${this.clientInstance.getClientId()}: ` +
`no 'result' field. ` +
`(response = ${JSON.stringify(response)})`,
name: `InvalidResponseError`,
Expand All @@ -142,4 +192,38 @@ export class PositronBaseComm {
dispose() {
this.clientInstance.dispose();
}

/**
* Create a new event emitter.
* @param name The name of the event, as a JSON-RPC method name.
* @param properties The names of the properties in the event payload; used
* to convert positional parameters to named parameters.
* @returns
*/
protected createEventEmitter<T>(
name: string,
properties: string[]
): Event<T> {
this.clientInstance.onDidSendEvent((event: any) => {
if (event.method === name) {
const args = event.params;
const namedArgs: any = {};
for (let i = 0; i < properties.length; i++) {
namedArgs[properties[i]] = args[i];
}
this._emitters.get(name)?.fire(namedArgs);
}
});

const emitter = new PositronCommEmitter<T>(name, properties);
this._emitters.set(name, emitter);
//this._register(emitter);
return emitter.event;
}

/**
* A map of event names to emitters. This is used to create event emitters
* from the backend to the frontend.
*/
private _emitters = new Map<string, PositronCommEmitter<any>>();
}
16 changes: 16 additions & 0 deletions extensions/positron-connections/src/comms/ConnectionsComms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { PositronBaseComm } from './BaseComm';
import * as positron from 'positron';
import { Event } from 'vscode';

//
// AUTO-GENERATED from connections.json; do not edit.
Expand Down Expand Up @@ -46,9 +47,20 @@ export interface FieldSchema {

}

/**
* Event: Request to focus the Connections pane
*/
export interface FocusEvent {
}

export enum ConnectionsFrontendEvent {
Focus = 'focus'
}

export class PositronConnectionsComm extends PositronBaseComm {
constructor(public instance: positron.RuntimeClientInstance) {
super(instance);
this.onDidFocus = super.createEventEmitter('focus', []);
}

/**
Expand Down Expand Up @@ -118,4 +130,8 @@ export class PositronConnectionsComm extends PositronBaseComm {
return super.performRpc('preview_object', ['path'], [path]);
}

/**
* Request to focus the Connections pane
*/
onDidFocus: Event<FocusEvent>;
}
4 changes: 4 additions & 0 deletions extensions/positron-connections/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ export class ConnectionItemsProvider implements vscode.TreeDataProvider<Connecti
this._onDidChangeTreeData.fire(undefined);
}
});

client.onDidFocus(() => {
vscode.commands.executeCommand('connections.focus', { preserveFocus: true });
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .access_keys import decode_access_key, encode_access_key
from .connections_comm import (
ConnectionsBackendMessageContent,
ConnectionsFrontendEvent,
ContainsDataRequest,
GetIconRequest,
ListFieldsRequest,
Expand Down Expand Up @@ -146,14 +147,18 @@ def __init__(self, kernel: PositronIPyKernel, comm_target_name: str):
self.comm_id_to_path: Dict[str, Set[PathKey]] = {}

def register_connection(
self, connection: Any, variable_path: Optional[List[str]] = None
self, connection: Any, variable_path: Optional[List[str]] = None, display_pane: bool = True
) -> str:
"""
Opens a connection to the given data source.
Args:
connection: A subclass of Connection implementing the
necessary methods.
variable_path: The variable path that points to the connection.
If None, the connection is not associated with any variable.
display_pane: Wether the Connection Pane view container should be
displayed in the UI once the connection is registered.
"""

if not isinstance(connection, Connection):
Expand All @@ -172,6 +177,10 @@ def register_connection(
comm_id,
)
self._register_variable_path(variable_path, comm_id)

if display_pane:
self.comms[comm_id].send_event(ConnectionsFrontendEvent.Focus.value, {})

return comm_id

comm_id = str(uuid.uuid4())
Expand All @@ -184,6 +193,10 @@ def register_connection(
self._register_variable_path(variable_path, comm_id)
self.comm_id_to_connection[comm_id] = connection
self.on_comm_open(base_comm)

if display_pane:
self.comms[comm_id].send_event(ConnectionsFrontendEvent.Focus.value, {})

return comm_id

def _register_variable_path(self, variable_path: Optional[List[str]], comm_id: str) -> None:
Expand All @@ -198,6 +211,10 @@ def _register_variable_path(self, variable_path: Optional[List[str]], comm_id: s
# a variable path can only point to a single connection, if it's already pointing
# to a connection, we "close the connection" and replace it with the new one
if key in self.path_to_comm_ids:
# if the variable path already points to the same comm_id, we don't need to
# perform any registration.
if self.path_to_comm_ids[key] == comm_id:
return
self._unregister_variable_path(key)

if comm_id in self.comm_id_to_path:
Expand Down Expand Up @@ -267,7 +284,8 @@ def handle_variable_updated(self, variable_name: str, value: Any) -> None:
try:
# registering a new connection with the same variable path is going to close the
# variable path if the connections are different.
self.register_connection(value, variable_path=variable_path)
# when handling a variable update we don't want to go and display the pane in the IDE
self.register_connection(value, variable_path=variable_path, display_pane=False)
except UnsupportedConnectionError:
# if an unsupported connection error, then it means the variable
# is no longer a connection, thus we unregister that variable path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ class ConnectionsBackendMessageContent(BaseModel):
] = Field(..., discriminator="method")


@enum.unique
class ConnectionsFrontendEvent(str, enum.Enum):
"""
An enumeration of all the possible events that can be sent to the frontend connections comm.
"""

# Request to focus the Connections pane
Focus = "focus"


ObjectSchema.update_forward_refs()

FieldSchema.update_forward_refs()
Expand Down
14 changes: 14 additions & 0 deletions positron/comms/connections-frontend-openrpc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"openrpc": "1.3.0",
"info": {
"title": "Connections frontend",
"version": "1.0.0"
},
"methods": [
{
"name": "focus",
"summary": "Request to focus the Connections pane",
"params": []
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// AUTO-GENERATED from connections.json; do not edit.
//

import { Event } from 'vs/base/common/event';
import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm';
import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance';

Expand Down Expand Up @@ -41,9 +42,20 @@ export interface FieldSchema {

}

/**
* Event: Request to focus the Connections pane
*/
export interface FocusEvent {
}

export enum ConnectionsFrontendEvent {
Focus = 'focus'
}

export class PositronConnectionsComm extends PositronBaseComm {
constructor(instance: IRuntimeClientInstance<any, any>) {
super(instance);
this.onDidFocus = super.createEventEmitter('focus', []);
}

/**
Expand Down Expand Up @@ -113,5 +125,10 @@ export class PositronConnectionsComm extends PositronBaseComm {
return super.performRpc('preview_object', ['path'], [path]);
}


/**
* Request to focus the Connections pane
*/
onDidFocus: Event<FocusEvent>;
}

0 comments on commit ae36eab

Please sign in to comment.