diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 861dca93303..be1b230a4fd 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -1101,6 +1101,59 @@ declare module 'positron' { pasteText(text: string): void; } + export enum ConnectionsInputType { + String = 'string', + Number = 'number', + Option = 'option', + } + + 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: ConnectionsInputType; + // Options, if the input type is an option. + options?: { 'identifier': string; 'title': string }[]; + // The default value for the input. + value?: string; + } + + 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; + } + + 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) => string; + // Connect session + connect?: (code: string) => Promise; + // Checks if the dependencies for the driver are installed + // and functioning. + checkDependencies?: () => Promise; + // Installs the dependencies for the driver. + // For instance, R packages would install the required + // R packages, and or other dependencies. + installDependencies?: () => Promise; + } + namespace languages { /** * Register a statement range provider. @@ -1387,4 +1440,11 @@ declare module 'positron' { } + + /** + * Refers to methods related to the connections pane + */ + namespace connections { + export function registerConnectionDriver(driver: ConnectionsDriver): vscode.Disposable; + } } diff --git a/src/vs/workbench/api/browser/positron/mainThreadConnections.ts b/src/vs/workbench/api/browser/positron/mainThreadConnections.ts new file mode 100644 index 00000000000..7a00acab31d --- /dev/null +++ b/src/vs/workbench/api/browser/positron/mainThreadConnections.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers'; +import { IDriver, IDriverMetadata, Input } from '../../../services/positronConnections/browser/interfaces/positronConnectionsDriver'; +import { IPositronConnectionsService } from '../../../services/positronConnections/browser/interfaces/positronConnectionsService'; +import { ExtHostConnectionsShape, ExtHostPositronContext, MainPositronContext, MainThreadConnectionsShape } from '../../common/positron/extHost.positron.protocol'; + +@extHostNamedCustomer(MainPositronContext.MainThreadConnections) +export class MainThreadConnections implements MainThreadConnectionsShape { + private readonly _proxy: ExtHostConnectionsShape; + 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 { + + } +} + +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; + } + } + get checkDependencies() { + if (!this.availableMethods.checkDependencies) { + return undefined; + } + } + get installDependencies() { + if (!this.availableMethods.installDependencies) { + return undefined; + } + } +} diff --git a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts index a8313dee1bd..9840e3d9b46 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts @@ -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. @@ -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 { @@ -192,6 +194,12 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce }, }; + const connections: typeof positron.connections = { + registerConnectionDriver(driver: positron.ConnectionsDriver): vscode.Disposable { + return extHostConnections.registerConnectionDriver(driver); + } + }; + // --- End Positron --- return { @@ -201,6 +209,7 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce window, languages, methods, + connections, PositronOutputLocation: extHostTypes.PositronOutputLocation, RuntimeClientType: extHostTypes.RuntimeClientType, RuntimeClientState: extHostTypes.RuntimeClientState, @@ -216,6 +225,7 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce RuntimeOnlineState: extHostTypes.RuntimeOnlineState, RuntimeState: extHostTypes.RuntimeState, RuntimeCodeFragmentStatus: extHostTypes.RuntimeCodeFragmentStatus, + ConnectionsInputType: extHostTypes.ConnectionsInputType, }; }; } diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index 4cff48fba29..a8cbb3f1b6e 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -11,6 +11,8 @@ import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IEditorContext } from '../../../services/frontendMethods/common/editorContext.js'; import { RuntimeClientType } from './extHostTypes.positron.js'; import { LanguageRuntimeDynState, RuntimeSessionMetadata } from 'positron'; +import { IDriverMetadata, Input } from '../../../services/positronConnections/browser/interfaces/positronConnectionsDriver.js'; +import { IAvailableDriverMethods } from '../../browser/positron/mainThreadConnections.js'; // NOTE: This check is really to ensure that extHost.protocol is included by the TypeScript compiler // as a dependency of this module, and therefore that it's initialized first. This is to avoid a @@ -107,6 +109,18 @@ export interface ExtHostMethodsShape { showQuestion(title: string, message: string, okButtonTitle: string, cancelButtonTitle: string): Promise; } +export interface MainThreadConnectionsShape { + $registerConnectionDriver(driverId: string, metadata: IDriverMetadata, availableMethods: IAvailableDriverMethods): void; + $removeConnectionDriver(driverId: string): void; +} + +export interface ExtHostConnectionsShape { + $driverGenerateCode(driverId: string, inputs: Input[]): Promise; + // $driverConnect(driverId: string, code: string): Promise; + // $driverCheckDependencies(driverId: string): Promise; + // $driverInstallDependencies(driverId: string): Promise; +} + /** * The view state of a preview in the Preview panel. Only one preview can be * active at a time (the one currently loaded into the panel); the active @@ -179,6 +193,7 @@ export const ExtHostPositronContext = { ExtHostConsoleService: createProxyIdentifier('ExtHostConsoleService'), ExtHostContextKeyService: createProxyIdentifier('ExtHostContextKeyService'), ExtHostMethods: createProxyIdentifier('ExtHostMethods'), + ExtHostConnections: createProxyIdentifier('ExtHostConnections'), }; export const MainPositronContext = { @@ -188,4 +203,5 @@ export const MainPositronContext = { MainThreadConsoleService: createProxyIdentifier('MainThreadConsoleService'), MainThreadContextKeyService: createProxyIdentifier('MainThreadContextKeyService'), MainThreadMethods: createProxyIdentifier('MainThreadMethods'), + MainThreadConnections: createProxyIdentifier('MainThreadConnections'), }; diff --git a/src/vs/workbench/api/common/positron/extHostConnections.ts b/src/vs/workbench/api/common/positron/extHostConnections.ts new file mode 100644 index 00000000000..84864ae9aa2 --- /dev/null +++ b/src/vs/workbench/api/common/positron/extHostConnections.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vscode'; +import * as positron from 'positron'; +import * as extHostProtocol from './extHost.positron.protocol.js'; +import { Input, InputType } from '../../../services/positronConnections/browser/interfaces/positronConnectionsDriver.js'; + +export class ExtHostConnections implements extHostProtocol.ExtHostConnectionsShape { + + private readonly _proxy: extHostProtocol.MainThreadConnectionsShape; + private _drivers: positron.ConnectionsDriver[] = []; + + constructor( + mainContext: extHostProtocol.IMainPositronContext, + ) { + // Trigger creation of the proxy + this._proxy = mainContext.getProxy(extHostProtocol.MainPositronContext.MainThreadConnections); + } + + public registerConnectionDriver(driver: positron.ConnectionsDriver): Disposable { + // Check if the driver is already registered, and if not push, otherwise replace + const existingDriverIndex = this._drivers.findIndex(d => d.driverId === driver.driverId); + if (existingDriverIndex !== -1) { + this._drivers[existingDriverIndex] = driver; + } else { + this._drivers.push(driver); + } + + const metadata = { + ...driver.metadata, + inputs: driver.metadata.inputs.map(i => extHost2MainThreadInput(i)) + } + + this._proxy.$registerConnectionDriver( + driver.driverId, + metadata, + { + generateCode: driver.generateCode ? true : false, + connect: driver.connect ? true : false, + checkDependencies: driver.checkDependencies ? true : false, + installDependencies: driver.installDependencies ? true : false + } + ); + return new Disposable(() => { }); + } + + public async $driverGenerateCode(driverId: string, inputs: Input[]) { + const driver = this._drivers.find(d => d.driverId === driverId); + if (!driver || !driver.generateCode) { + throw new Error(`Driver ${driverId} does not support code generation`); + } + return driver.generateCode(inputs.map( + i => ({ ...i, type: i.type as string as positron.ConnectionsInputType }) + )); + } + + public $driverConnect(driverId: string, code: string): Promise { + const driver = this._drivers.find(d => d.driverId === driverId); + if (!driver || !driver.connect) { + throw new Error(`Driver ${driverId} does not support connecting`); + } + return driver.connect(code); + } + + public $driverCheckDependencies(driverId: string): Promise { + const driver = this._drivers.find(d => d.driverId === driverId); + if (!driver || !driver.checkDependencies) { + throw new Error(`Driver ${driverId} does not support checking dependencies`); + } + return driver.checkDependencies(); + } + + public $driverInstallDependencies(driverId: string): Promise { + const driver = this._drivers.find(d => d.driverId === driverId); + if (!driver || !driver.installDependencies) { + throw new Error(`Driver ${driverId} does not support installing dependencies`); + } + return driver.installDependencies(); + } +} + +function extHost2MainThreadInput(input: positron.ConnectionsInput): Input { + return { + ...input, + type: input.type as string as InputType + } +} diff --git a/src/vs/workbench/api/common/positron/extHostTypes.positron.ts b/src/vs/workbench/api/common/positron/extHostTypes.positron.ts index a556393b5b8..ecdedd2771b 100644 --- a/src/vs/workbench/api/common/positron/extHostTypes.positron.ts +++ b/src/vs/workbench/api/common/positron/extHostTypes.positron.ts @@ -314,3 +314,9 @@ export enum LanguageRuntimeSessionLocation { */ Browser = 'browser', } + +export enum ConnectionsInputType { + String = 'string', + Number = 'number', + Option = 'option', +} diff --git a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog.tsx b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog.tsx index 9bfad51005c..42799e3fb77 100644 --- a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog.tsx +++ b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog.tsx @@ -56,7 +56,7 @@ const NewConnectionModalDialog = (props: PropsWithChildren { // When hitting back, reset the language ID to the previously selected language id - setLanguageId(selectedDriver?.languageId); + setLanguageId(selectedDriver?.metadata.languageId); setSelectedDriver(undefined); }; diff --git a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx index d4bf60a2237..dff0f76dfea 100644 --- a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx +++ b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx @@ -30,18 +30,19 @@ interface CreateConnectionProps { export const CreateConnection = (props: PropsWithChildren) => { - const { name, languageId, generateCode } = props.selectedDriver; + const { generateCode, metadata } = props.selectedDriver; + const { name, languageId } = metadata; const { onBack, onCancel, services } = props; const editorRef = useRef(undefined!); - const [inputs, setInputs] = useState>(props.selectedDriver.inputs); - const [code, setCode] = useState(props.selectedDriver.generateCode?.(props.selectedDriver.inputs)); + const [inputs, setInputs] = useState>(metadata.inputs); + const [code, setCode] = useState(undefined); useEffect(() => { // Debounce the code generation to avoid unnecessary re-renders - const timeoutId = setTimeout(() => { + const timeoutId = setTimeout(async () => { if (generateCode) { - const code = generateCode(inputs); + const code = await generateCode(inputs); setCode(code); } }, 200); @@ -57,7 +58,7 @@ export const CreateConnection = (props: PropsWithChildren message: localize( 'positron.newConnectionModalDialog.createConnection.connecting', "Connecting to data source ({0})...", - props.selectedDriver.name + name ), severity: Severity.Info }); @@ -96,7 +97,7 @@ export const CreateConnection = (props: PropsWithChildren -
+
{(() => localize('positron.newConnectionModalDialog.createConnection.code', "Connection Code"))()} diff --git a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/listDriversState.tsx b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/listDriversState.tsx index 6d2f300be12..8b97696d8ba 100644 --- a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/listDriversState.tsx +++ b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/listDriversState.tsx @@ -36,7 +36,7 @@ export const ListDrivers = (props: PropsWithChildren) => { const driverManager = props.services.connectionsService.driverManager; const drivers = languageId ? - driverManager.getDrivers().filter(driver => driver.languageId === languageId) : + driverManager.getDrivers().filter(driver => driver.metadata.languageId === languageId) : []; const onLanguageChangeHandler = (lang: string) => { @@ -79,15 +79,15 @@ export const ListDrivers = (props: PropsWithChildren) => { { drivers.length > 0 ? drivers.map(driver => { - const icon = driver.base64EncodedIconSvg ? - : + const icon = driver.metadata.base64EncodedIconSvg ? + :
; return
{icon}
onDriverSelectedHandler(driver)}>
- {driver.name} + {driver.metadata.name}
diff --git a/src/vs/workbench/services/positronConnections/browser/interfaces/positronConnectionsDriver.ts b/src/vs/workbench/services/positronConnections/browser/interfaces/positronConnectionsDriver.ts index 2fee6d820d8..7ca100bd9d9 100644 --- a/src/vs/workbench/services/positronConnections/browser/interfaces/positronConnectionsDriver.ts +++ b/src/vs/workbench/services/positronConnections/browser/interfaces/positronConnectionsDriver.ts @@ -22,9 +22,7 @@ export interface Input { value?: string; } -export interface IDriver { - // The unique identifier for the driver. - driverId: string; +export interface IDriverMetadata { // The language identifier for the driver. // Drivers are grouped by language, not by runtime. languageId: string; @@ -36,8 +34,17 @@ export interface IDriver { // For instance, a connection might require a username // and password. inputs: Array; +} + +export interface IDriver { + // The unique identifier for the driver. + driverId: string; + + // The metadata for the driver. + metadata: IDriverMetadata; + // Generates the connection code based on the inputs. - generateCode?: (inputs: Array) => string; + generateCode?: (inputs: Array) => Promise; // Connect session connect?: (code: string) => Promise; // Checks if the dependencies for the driver are installed diff --git a/src/vs/workbench/services/positronConnections/browser/positronConnectionsDrivers.ts b/src/vs/workbench/services/positronConnections/browser/positronConnectionsDrivers.ts index ac5e78c12c3..19f3c086cab 100644 --- a/src/vs/workbench/services/positronConnections/browser/positronConnectionsDrivers.ts +++ b/src/vs/workbench/services/positronConnections/browser/positronConnectionsDrivers.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { RuntimeCodeExecutionMode, RuntimeErrorBehavior } from '../../languageRuntime/common/languageRuntimeService.js'; -import { IDriver, Input, InputType } from './interfaces/positronConnectionsDriver.js'; +import { IDriver, IDriverMetadata, Input, InputType } from './interfaces/positronConnectionsDriver.js'; import { IPositronConnectionsService } from './interfaces/positronConnectionsService.js'; import { ILanguageRuntimeSession } from '../../runtimeSession/common/runtimeSessionService.js'; @@ -25,6 +25,13 @@ export class PositronConnectionsDriverManager { } } + removeDriver(driverId: string): void { + const index = this.drivers.findIndex(d => d.driverId === driverId); + if (index > 0) { + this.drivers.splice(index, 1); + } + } + getDrivers(): IDriver[] { return this.drivers; } @@ -38,55 +45,58 @@ export class PositronConnectionsDriverManager { class RPostgreSQLDriver implements IDriver { constructor(readonly service: IPositronConnectionsService) { } - languageId: string = 'r'; driverId: string = 'postgres'; - name: string = 'PostgresSQL'; - inputs: Input[] = [ - { - 'id': 'dbname', - 'label': 'Database Name', - 'type': InputType.String, - 'value': 'localhost' - }, - { - 'id': 'host', - 'label': 'Host', - 'type': InputType.String, - 'value': 'localhost' - }, - { - 'id': 'port', - 'label': 'Port', - 'type': InputType.Number, - 'value': '5432' - }, - { - 'id': 'user', - 'label': 'User', - 'type': InputType.String, - 'value': 'postgres' - }, - { - 'id': 'password', - 'label': 'Password', - 'type': InputType.String, - 'value': 'password' - }, - { - 'id': 'bigint', - 'label': 'Integer representation', - 'type': InputType.Option, - 'options': [ - { 'identifier': 'integer64', 'title': 'integer64' }, - { 'identifier': 'integer', 'title': 'integer' }, - { 'identifier': 'numeric', 'title': 'numeric' }, - { 'identifier': 'character', 'title': 'character' } - ], - 'value': 'integer64' - } - ]; - generateCode(inputs: Array) { + metadata: IDriverMetadata = { + languageId: 'r', + name: 'PostgresSQL', + inputs: [ + { + 'id': 'dbname', + 'label': 'Database Name', + 'type': InputType.String, + 'value': 'localhost' + }, + { + 'id': 'host', + 'label': 'Host', + 'type': InputType.String, + 'value': 'localhost' + }, + { + 'id': 'port', + 'label': 'Port', + 'type': InputType.Number, + 'value': '5432' + }, + { + 'id': 'user', + 'label': 'User', + 'type': InputType.String, + 'value': 'postgres' + }, + { + 'id': 'password', + 'label': 'Password', + 'type': InputType.String, + 'value': 'password' + }, + { + 'id': 'bigint', + 'label': 'Integer representation', + 'type': InputType.Option, + 'options': [ + { 'identifier': 'integer64', 'title': 'integer64' }, + { 'identifier': 'integer', 'title': 'integer' }, + { 'identifier': 'numeric', 'title': 'numeric' }, + { 'identifier': 'character', 'title': 'character' } + ], + 'value': 'integer64' + } + ] + } + + async generateCode(inputs: Array) { 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;