From fd367e42c24e442c2e2dec07f1c5e8098f965d76 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Mon, 29 Jul 2024 18:29:20 -0600 Subject: [PATCH] Add OpenRPC method for evaluating a `when` clause (#4165) Goes along with https://github.com/posit-dev/ark/pull/449 I was thinking about how to address https://github.com/posit-dev/positron/issues/2697 and have a proposed approach in this PR plus the accompanying PR for ark. I looked at how various extensions (like the git extension, etc) check for whether we are in a git repo in the workspace and all that checking is typically based on context keys. In an extension specifically, you can check those keys with a ["when" clause](https://code.visualstudio.com/api/references/when-clause-contexts) and I realized that approach (i.e. a string) could work well as an OpenRPC method and would be flexible for other context keys we will need to check when we are not in the main thread. Alternatively, we could add methods to return more specific info (are we in a git repo? etc etc etc) but I like the idea of using "when" clauses for this, like an extension could directly for commands, etc. ### QA Notes Evaluating code like this in R should show the correct results for your situation: ```r .ps.ui.evaluateWhenClause("isLinux || isWindows") .ps.ui.evaluateWhenClause("isMac") .ps.ui.evaluateWhenClause("gitOpenRepositoryCount >= 1") ``` --------- Signed-off-by: Julia Silge --- .../positron/positron_ipykernel/ui_comm.py | 12 +++++++ extensions/positron-r/package.json | 2 +- positron/comms/ui-frontend-openrpc.json | 20 +++++++++++ .../api/browser/extensionHost.contribution.ts | 1 + .../positron/mainThreadContextKeyService.ts | 34 +++++++++++++++++++ .../positron/extHost.positron.api.impl.ts | 5 ++- .../positron/extHost.positron.protocol.ts | 10 ++++++ .../positron/extHostContextKeyService.ts | 29 ++++++++++++++++ .../api/common/positron/extHostMethods.ts | 15 +++++++- .../languageRuntime/common/positronUiComm.ts | 14 ++++++++ 10 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/vs/workbench/api/browser/positron/mainThreadContextKeyService.ts create mode 100644 src/vs/workbench/api/common/positron/extHostContextKeyService.ts diff --git a/extensions/positron-python/python_files/positron/positron_ipykernel/ui_comm.py b/extensions/positron-python/python_files/positron/positron_ipykernel/ui_comm.py index dda73094a4e..4ace3d0e8ec 100644 --- a/extensions/positron-python/python_files/positron/positron_ipykernel/ui_comm.py +++ b/extensions/positron-python/python_files/positron/positron_ipykernel/ui_comm.py @@ -355,6 +355,16 @@ class ExecuteCommandParams(BaseModel): ) +class EvaluateWhenClauseParams(BaseModel): + """ + Get a logical for a `when` clause (a set of context keys) + """ + + when_clause: StrictStr = Field( + description="The values for context keys, as a `when` clause", + ) + + class ExecuteCodeParams(BaseModel): """ Execute code in a Positron runtime @@ -481,6 +491,8 @@ class ShowHtmlFileParams(BaseModel): ExecuteCommandParams.update_forward_refs() +EvaluateWhenClauseParams.update_forward_refs() + ExecuteCodeParams.update_forward_refs() OpenWorkspaceParams.update_forward_refs() diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index fd34acde7f0..c152a0a713e 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -606,7 +606,7 @@ }, "positron": { "binaryDependencies": { - "ark": "0.1.119" + "ark": "0.1.120" }, "minimumRVersion": "4.2.0", "minimumRenvVersion": "1.0.7" diff --git a/positron/comms/ui-frontend-openrpc.json b/positron/comms/ui-frontend-openrpc.json index eb748374af7..012b782f5ff 100644 --- a/positron/comms/ui-frontend-openrpc.json +++ b/positron/comms/ui-frontend-openrpc.json @@ -216,6 +216,26 @@ } ] }, + { + "name": "evaluate_when_clause", + "summary": "Get a logical for a `when` clause (a set of context keys)", + "description": "Use this to evaluate a `when` clause of context keys in the frontend", + "params": [ + { + "name": "when_clause", + "description": "The values for context keys, as a `when` clause", + "schema": { + "type": "string" + } + } + ], + "result": { + "schema": { + "type": "boolean", + "description": "Whether the `when` clause evaluates as true or false" + } + } + }, { "name": "execute_code", "summary": "Execute code in a Positron runtime", diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index a49081a78f1..6f1c8c57525 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -94,6 +94,7 @@ import './positron/mainThreadLanguageRuntime'; import './positron/mainThreadPreviewPanel'; import './positron/mainThreadModalDialogs'; import './positron/mainThreadConsoleService'; +import './positron/mainThreadContextKeyService'; // --- End Positron --- export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/positron/mainThreadContextKeyService.ts b/src/vs/workbench/api/browser/positron/mainThreadContextKeyService.ts new file mode 100644 index 00000000000..c9d77b3670e --- /dev/null +++ b/src/vs/workbench/api/browser/positron/mainThreadContextKeyService.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { MainPositronContext, MainThreadContextKeyServiceShape } from '../../common/positron/extHost.positron.protocol'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { DisposableStore } from 'vs/base/common/lifecycle'; + +@extHostNamedCustomer(MainPositronContext.MainThreadContextKeyService) +export class MainThreadContextKeyService implements MainThreadContextKeyServiceShape { + + private readonly _disposables = new DisposableStore(); + + constructor( + extHostContext: IExtHostContext, + @IContextKeyService private readonly contextKeyService: IContextKeyService + ) { + } + + $evaluateWhenClause(whenClause: string): Promise { + const precondition = ContextKeyExpr.deserialize(whenClause); + if (precondition === undefined) { + throw new Error(`Cannot evaluate when clause '${whenClause}'`); + } + return Promise.resolve(this.contextKeyService.contextMatchesRules(precondition)); + } + + public dispose(): void { + this._disposables.dispose(); + } + +} 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 9128ad2052a..c4410d13acf 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 @@ -17,6 +17,7 @@ import * as extHostTypes from 'vs/workbench/api/common/positron/extHostTypes.pos import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { ExtHostPreviewPanels } from 'vs/workbench/api/common/positron/extHostPreviewPanels'; import { ExtHostModalDialogs } from 'vs/workbench/api/common/positron/extHostModalDialogs'; +import { ExtHostContextKeyService } from 'vs/workbench/api/common/positron/extHostContextKeyService'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; @@ -63,8 +64,10 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce const extHostLanguageRuntime = rpcProtocol.set(ExtHostPositronContext.ExtHostLanguageRuntime, new ExtHostLanguageRuntime(rpcProtocol, extHostLogService)); const extHostPreviewPanels = rpcProtocol.set(ExtHostPositronContext.ExtHostPreviewPanel, new ExtHostPreviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostModalDialogs = rpcProtocol.set(ExtHostPositronContext.ExtHostModalDialogs, new ExtHostModalDialogs(rpcProtocol)); + const extHostContextKeyService = rpcProtocol.set(ExtHostPositronContext.ExtHostContextKeyService, new ExtHostContextKeyService(rpcProtocol)); const extHostConsoleService = rpcProtocol.set(ExtHostPositronContext.ExtHostConsoleService, new ExtHostConsoleService(rpcProtocol, extHostLogService)); - const extHostMethods = rpcProtocol.set(ExtHostPositronContext.ExtHostMethods, new ExtHostMethods(rpcProtocol, extHostEditors, extHostDocuments, extHostModalDialogs, extHostLanguageRuntime, extHostWorkspace)); + const extHostMethods = rpcProtocol.set(ExtHostPositronContext.ExtHostMethods, + new ExtHostMethods(rpcProtocol, extHostEditors, extHostDocuments, extHostModalDialogs, extHostLanguageRuntime, extHostWorkspace, extHostContextKeyService)); return function (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof positron { 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 5f7e760bfeb..eafea6fbc66 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -79,6 +79,14 @@ export interface MainThreadModalDialogsShape extends IDisposable { // The interface to the main thread exposed by the extension host export interface ExtHostModalDialogsShape { } +// Interface that the main process exposes to the extension host +export interface MainThreadContextKeyServiceShape { + $evaluateWhenClause(whenClause: string): Promise; +} + +// Interface to the main thread exposed by the extension host +export interface ExtHostContextKeyServiceShape { } + export interface MainThreadConsoleServiceShape { $getConsoleWidth(): Promise; $tryPasteText(id: string, text: string): void; @@ -168,6 +176,7 @@ export const ExtHostPositronContext = { ExtHostPreviewPanel: createProxyIdentifier('ExtHostPreviewPanel'), ExtHostModalDialogs: createProxyIdentifier('ExtHostModalDialogs'), ExtHostConsoleService: createProxyIdentifier('ExtHostConsoleService'), + ExtHostContextKeyService: createProxyIdentifier('ExtHostContextKeyService'), ExtHostMethods: createProxyIdentifier('ExtHostMethods'), }; @@ -176,5 +185,6 @@ export const MainPositronContext = { MainThreadPreviewPanel: createProxyIdentifier('MainThreadPreviewPanel'), MainThreadModalDialogs: createProxyIdentifier('MainThreadModalDialogs'), MainThreadConsoleService: createProxyIdentifier('MainThreadConsoleService'), + MainThreadContextKeyService: createProxyIdentifier('MainThreadContextKeyService'), MainThreadMethods: createProxyIdentifier('MainThreadMethods'), }; diff --git a/src/vs/workbench/api/common/positron/extHostContextKeyService.ts b/src/vs/workbench/api/common/positron/extHostContextKeyService.ts new file mode 100644 index 00000000000..61278749ca7 --- /dev/null +++ b/src/vs/workbench/api/common/positron/extHostContextKeyService.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as extHostProtocol from './extHost.positron.protocol'; + +export class ExtHostContextKeyService implements extHostProtocol.ExtHostContextKeyServiceShape { + + private readonly _proxy: extHostProtocol.MainThreadContextKeyServiceShape; + + constructor( + mainContext: extHostProtocol.IMainPositronContext, + ) { + // Trigger creation of the proxy + this._proxy = mainContext.getProxy(extHostProtocol.MainPositronContext.MainThreadContextKeyService); + } + + /** + * Queries the main thread with a `when` clause. + * + * @returns If the `when` clause evaluates to true or false. + */ + public evaluateWhenClause(whenClause: string): Promise { + return this._proxy.$evaluateWhenClause(whenClause); + } + +} + diff --git a/src/vs/workbench/api/common/positron/extHostMethods.ts b/src/vs/workbench/api/common/positron/extHostMethods.ts index 7237a298dbe..36b8adf92dc 100644 --- a/src/vs/workbench/api/common/positron/extHostMethods.ts +++ b/src/vs/workbench/api/common/positron/extHostMethods.ts @@ -8,6 +8,7 @@ import { ExtHostEditors } from '../extHostTextEditors'; import { ExtHostDocuments } from '../extHostDocuments'; import { ExtHostWorkspace } from '../extHostWorkspace'; import { ExtHostModalDialogs } from '../positron/extHostModalDialogs'; +import { ExtHostContextKeyService } from '../positron/extHostContextKeyService'; import { ExtHostLanguageRuntime } from '../positron/extHostLanguageRuntime'; import { UiFrontendRequest, EditorContext, Range as UIRange } from 'vs/workbench/services/languageRuntime/common/positronUiComm'; import { JsonRpcErrorCode } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; @@ -41,7 +42,8 @@ export class ExtHostMethods implements extHostProtocol.ExtHostMethodsShape { private readonly documents: ExtHostDocuments, private readonly dialogs: ExtHostModalDialogs, private readonly runtime: ExtHostLanguageRuntime, - private readonly workspace: ExtHostWorkspace + private readonly workspace: ExtHostWorkspace, + private readonly contextKeys: ExtHostContextKeyService ) { } @@ -137,6 +139,13 @@ export class ExtHostMethods implements extHostProtocol.ExtHostMethodsShape { params.allow_incomplete as boolean); break; } + case UiFrontendRequest.EvaluateWhenClause: { + if (!params || !Object.keys(params).includes('when_clause')) { + return newInvalidParamsError(method); + } + result = await this.evaluateWhenClause(params.when_clause as string); + break; + } case UiFrontendRequest.DebugSleep: { if (!params || !Object.keys(params).includes('ms')) { return newInvalidParamsError(method); @@ -270,6 +279,10 @@ export class ExtHostMethods implements extHostProtocol.ExtHostMethodsShape { return this.runtime.executeCode(languageId, code, focus, allowIncomplete); } + async evaluateWhenClause(whenClause: string): Promise { + return this.contextKeys.evaluateWhenClause(whenClause); + } + async debugSleep(ms: number): Promise { await delay(ms); return null; diff --git a/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts b/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts index 18a816a6def..5ab75d31225 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts @@ -387,6 +387,19 @@ export interface DebugSleepRequest { } +/** + * Request: Get a logical for a `when` clause (a set of context keys) + * + * Use this to evaluate a `when` clause of context keys in the frontend + */ +export interface EvaluateWhenClauseRequest { + /** + * The values for context keys, as a `when` clause + */ + when_clause: string; + +} + /** * Request: Execute code in a Positron runtime * @@ -470,6 +483,7 @@ export enum UiFrontendRequest { ShowQuestion = 'show_question', ShowDialog = 'show_dialog', DebugSleep = 'debug_sleep', + EvaluateWhenClause = 'evaluate_when_clause', ExecuteCode = 'execute_code', WorkspaceFolder = 'workspace_folder', ModifyEditorSelections = 'modify_editor_selections',