Skip to content

Commit

Permalink
API for extending the connections pane (#5785)
Browse files Browse the repository at this point in the history
Addresses: #5689

Add a positron public API that can be used to register new connection
drivers. Registered drivers are used by the connections pane 'New
Connection' button.
  • Loading branch information
dfalbel authored Dec 20, 2024
1 parent 6edeff5 commit 18a1951
Show file tree
Hide file tree
Showing 24 changed files with 466 additions and 173 deletions.
102 changes: 102 additions & 0 deletions extensions/positron-connections/src/drivers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as positron from 'positron';
import * as vscode from 'vscode';

export function registerConnectionDrivers(context: vscode.ExtensionContext) {
context.subscriptions.push(
positron.connections.registerConnectionDriver(new RPostgreSQLDriver())
);
}

class RPostgreSQLDriver implements positron.ConnectionsDriver {
driverId: string = 'postgres';
metadata: positron.ConnectionsDriverMetadata = {
languageId: 'r',
name: 'PostgresSQL',
inputs: [
{
'id': 'dbname',
'label': 'Database Name',
'type': 'string',
'value': 'localhost'
},
{
'id': 'host',
'label': 'Host',
'type': 'string',
'value': 'localhost'
},
{
'id': 'port',
'label': 'Port',
'type': 'number',
'value': '5432'
},
{
'id': 'user',
'label': 'User',
'type': 'string',
'value': 'postgres'
},
{
'id': 'password',
'label': 'Password',
'type': 'string',
'value': 'password'
},
{
'id': 'bigint',
'label': 'Integer representation',
'type': 'option',
'options': [
{ 'identifier': 'integer64', 'title': 'integer64' },
{ 'identifier': 'integer', 'title': 'integer' },
{ 'identifier': 'numeric', 'title': 'numeric' },
{ 'identifier': 'character', 'title': 'character' }
],
'value': 'integer64'
}
]
};

generateCode(inputs: positron.ConnectionsInput[]) {
const dbname = inputs.find(input => input.id === 'dbname')?.value;
const host = inputs.find(input => input.id === 'host')?.value;
const port = inputs.find(input => input.id === 'port')?.value;
const user = inputs.find(input => input.id === 'user')?.value;
const password = inputs.find(input => input.id === 'password')?.value;
const bigint = inputs.find(input => input.id === 'bigint')?.value;

return `library(DBI)
con <- dbConnect(
RPostgres::Postgres(),
dbname = '${dbname ?? ''}',
host = '${host ?? ''}',
port = ${port ?? ''},
user = '${user ?? ''}',
password = '${password ?? ''}',
bigint = '${bigint ?? ''}'
)
`;
}

async connect(code: string) {
const exec = await positron.runtime.executeCode(
'r',
code,
true,
false,
positron.RuntimeCodeExecutionMode.Interactive,
positron.RuntimeErrorBehavior.Continue
);
if (!exec) {
throw new Error('Failed to execute code');
}
return;
}
}

3 changes: 3 additions & 0 deletions extensions/positron-connections/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import * as vscode from 'vscode';
import * as positron from 'positron';
import { ConnectionItem, ConnectionItemsProvider, isActiveConnectionItem, DatabaseConnectionItem, DisconnectedConnectionItem } from './connection';
import { PositronConnectionsComm } from './comms/ConnectionsComms';
import { registerConnectionDrivers } from './drivers';


export function activate(context: vscode.ExtensionContext) {
// We always register the drivers.
registerConnectionDrivers(context);

const config = vscode.workspace.getConfiguration('positron');
const enabled = !config.get<boolean>('connections', false);
Expand Down
100 changes: 100 additions & 0 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,92 @@ declare module 'positron' {
pasteText(text: string): void;
}

/**
* ConnectionsInput interface defines the structure for connection inputs.
*/
export interface ConnectionsInput {
/**
* The unique identifier for the input.
*/
id: string;
/**
* A human-readable label for the input.
*/
label: string;
/**
* The type of the input.
*/
type: 'string' | 'number' | 'option';
/**
* Options, if the input type is an option.
*/
options?: { 'identifier': string; 'title': string }[];
/**
* The default value for the input.
*/
value?: string;
}

/**
* ConnectionsDriverMetadata interface defines the structure for connection driver metadata.
*/
export interface ConnectionsDriverMetadata {
/**
* The language identifier for the driver.
* Drivers are grouped by language, not by runtime.
*/
languageId: string;
/**
* A human-readable name for the driver.
*/
name: string;
/**
* The base64-encoded SVG icon for the driver.
*/
base64EncodedIconSvg?: string;
/**
* The inputs required to create a connection.
* For instance, a connection might require a username
* and password.
*/
inputs: Array<ConnectionsInput>;
}

export interface ConnectionsDriver {
/**
* The unique identifier for the driver.
*/
driverId: string;

/**
* The metadata for the driver.
*/
metadata: ConnectionsDriverMetadata;

/**
* Generates the connection code based on the inputs.
*/
generateCode?: (inputs: Array<ConnectionsInput>) => string;

/**
* Connect session.
*/
connect?: (code: string) => Promise<void>;

/**
* Checks if the dependencies for the driver are installed
* and functioning.
*/
checkDependencies?: () => Promise<boolean>;

/**
* Installs the dependencies for the driver.
* For instance, R packages would install the required
* R packages, and or other dependencies.
*/
installDependencies?: () => Promise<boolean>;
}

namespace languages {
/**
* Register a statement range provider.
Expand Down Expand Up @@ -1387,4 +1473,18 @@ declare module 'positron' {


}

/**
* Refers to methods related to the connections pane
*/
namespace connections {
/**
* Registers a new connection driver with Positron allowing extensions to contribute
* to the 'New Connection' dialog.
*
* @param driver The connection driver to register
* @returns A disposable that unregisters the driver when disposed
*/
export function registerConnectionDriver(driver: ConnectionsDriver): vscode.Disposable;
}
}
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import './positron/mainThreadPreviewPanel.js';
import './positron/mainThreadModalDialogs.js';
import './positron/mainThreadConsoleService.js';
import './positron/mainThreadContextKeyService.js';
import './positron/mainThreadConnections.js';
// --- End Positron ---

export class ExtensionPoints implements IWorkbenchContribution {
Expand Down
76 changes: 76 additions & 0 deletions src/vs/workbench/api/browser/positron/mainThreadConnections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import { ExtHostConnectionsShape, ExtHostPositronContext, MainPositronContext, MainThreadConnectionsShape } from '../../common/positron/extHost.positron.protocol.js';
import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js';
import { IDriver, IDriverMetadata, Input } from '../../../services/positronConnections/common/interfaces/positronConnectionsDriver.js';
import { IPositronConnectionsService } from '../../../services/positronConnections/common/interfaces/positronConnectionsService.js';
import { DisposableStore } from '../../../../base/common/lifecycle.js';

@extHostNamedCustomer(MainPositronContext.MainThreadConnections)
export class MainThreadConnections implements MainThreadConnectionsShape {
private readonly _proxy: ExtHostConnectionsShape;
private readonly _disposables = new DisposableStore();
constructor(
extHostContext: IExtHostContext,
@IPositronConnectionsService private readonly _connectionsService: IPositronConnectionsService
) {
this._proxy = extHostContext.getProxy(ExtHostPositronContext.ExtHostConnections);
}

$registerConnectionDriver(driverId: string, metadata: IDriverMetadata, availableMethods: IAvailableDriverMethods): void {
this._connectionsService.driverManager.registerDriver(new MainThreadDriverAdapter(
driverId, metadata, availableMethods, this._proxy
));
}

$removeConnectionDriver(driverId: string): void {
this._connectionsService.driverManager.removeDriver(driverId);
}

dispose(): void {
this._disposables.dispose();
}
}

export interface IAvailableDriverMethods {
generateCode: boolean,
connect: boolean,
checkDependencies: boolean,
installDependencies: boolean
}

class MainThreadDriverAdapter implements IDriver {
constructor(
readonly driverId: string,
readonly metadata: IDriverMetadata,
private readonly availableMethods: IAvailableDriverMethods,
private readonly _proxy: ExtHostConnectionsShape
) { }
get generateCode() {
if (!this.availableMethods.generateCode) {
return undefined;
}
return (inputs: Input[]) => this._proxy.$driverGenerateCode(this.driverId, inputs);
}
get connect() {
if (!this.availableMethods.connect) {
return undefined;
}
return (code: string) => this._proxy.$driverConnect(this.driverId, code);
}
get checkDependencies() {
if (!this.availableMethods.checkDependencies) {
return undefined;
}
return () => this._proxy.$driverCheckDependencies(this.driverId);
}
get installDependencies() {
if (!this.availableMethods.installDependencies) {
return undefined;
}
return () => this._proxy.$driverInstallDependencies(this.driverId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { IRuntimeStartupService, RuntimeStartupPhase } from '../../../services/r
import { SerializableObjectWithBuffers } from '../../../services/extensions/common/proxyIdentifier.js';
import { isWebviewReplayMessage } from '../../../contrib/positronWebviewPreloads/browser/utils.js';
import { IPositronWebviewPreloadService } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js';
import { IPositronConnectionsService } from '../../../services/positronConnections/browser/interfaces/positronConnectionsService.js';
import { IPositronConnectionsService } from '../../../services/positronConnections/common/interfaces/positronConnectionsService.js';

/**
* Represents a language runtime event (for example a message or state change)
Expand Down
15 changes: 15 additions & 0 deletions src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ExtHostConsoleService } from './extHostConsoleService.js';
import { ExtHostMethods } from './extHostMethods.js';
import { ExtHostEditors } from '../extHostTextEditors.js';
import { UiFrontendRequest } from '../../../services/languageRuntime/common/positronUiComm.js';
import { ExtHostConnections } from './extHostConnections.js';

/**
* Factory interface for creating an instance of the Positron API.
Expand Down Expand Up @@ -71,6 +72,7 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
const extHostMethods = rpcProtocol.set(ExtHostPositronContext.ExtHostMethods,
new ExtHostMethods(rpcProtocol, extHostEditors, extHostDocuments, extHostModalDialogs,
extHostLanguageRuntime, extHostWorkspace, extHostCommands, extHostContextKeyService));
const extHostConnections = rpcProtocol.set(ExtHostPositronContext.ExtHostConnections, new ExtHostConnections(rpcProtocol));

return function (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof positron {

Expand Down Expand Up @@ -192,6 +194,18 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
},
};

const connections: typeof positron.connections = {
/**
* Register a connection driver that's used to generate code for connecting to a data source
* using the 'New Connection' dialog.
* @param driver The connection driver to register.
* @returns A disposable that can be used to unregister the driver.
*/
registerConnectionDriver(driver: positron.ConnectionsDriver): vscode.Disposable {
return extHostConnections.registerConnectionDriver(driver);
}
};

// --- End Positron ---

return <typeof positron>{
Expand All @@ -201,6 +215,7 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
window,
languages,
methods,
connections,
PositronOutputLocation: extHostTypes.PositronOutputLocation,
RuntimeClientType: extHostTypes.RuntimeClientType,
RuntimeClientState: extHostTypes.RuntimeClientState,
Expand Down
Loading

0 comments on commit 18a1951

Please sign in to comment.