Skip to content

Commit 02b7f84

Browse files
committed
feat: code lens + new "SFDX: Get SOQL Query Plan with Current File" command in commmand palette
1 parent 911dc88 commit 02b7f84

8 files changed

Lines changed: 117 additions & 5 deletions

File tree

packages/salesforcedx-vscode-soql/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@
357357
{
358358
"command": "sf.data.query.explain.selection",
359359
"title": "%query_plan_selection_text%"
360+
},
361+
{
362+
"command": "sf.data.query.explain.document",
363+
"title": "%query_plan_document_text%"
360364
}
361365
],
362366
"menus": {
@@ -388,6 +392,10 @@
388392
{
389393
"command": "sf.data.query.explain.selection",
390394
"when": "sf:project_opened && editorHasSelection && sf:has_target_org"
395+
},
396+
{
397+
"command": "sf.data.query.explain.document",
398+
"when": "resourceExtname == .soql && sf:has_target_org"
391399
}
392400
],
393401
"editor/title": [
@@ -466,5 +474,8 @@
466474
]
467475
}
468476
]
469-
}
477+
},
478+
"activationEvents": [
479+
"onLanguage:soql"
480+
]
470481
}

packages/salesforcedx-vscode-soql/package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525
"soql_walkthrough_step5_alt": "Saving and exporting SOQL query results",
2626
"data_query_input_text": "SFDX: Execute SOQL Query...",
2727
"data_query_selection_text": "SFDX: Execute SOQL Query with Currently Selected Text",
28-
"query_plan_selection_text": "SFDX: Get SOQL Query Plan with Currently Selected Text"
28+
"query_plan_selection_text": "SFDX: Get SOQL Query Plan with Currently Selected Text",
29+
"query_plan_document_text": "SFDX: Get SOQL Query Plan with Current File"
2930
}

packages/salesforcedx-vscode-soql/src/commands/queryPlan.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as vscode from 'vscode';
1111
import { nls } from '../messages';
1212
import { channelService } from '../services/channel';
1313
import { getConnection } from '../services/org';
14-
import { formatErrorMessage, GetQueryAndApiInputs, QueryAndApiInputs } from './queryUtils';
14+
import { formatErrorMessage, GetDocumentQueryAndApiInputs, GetQueryAndApiInputs, QueryAndApiInputs } from './queryUtils';
1515

1616
class QueryPlanExecutor {
1717
public async execute(response: ContinueResponse<QueryAndApiInputs>): Promise<void> {
@@ -47,3 +47,12 @@ export const queryPlan = Effect.fn('sf.data.query.explain')(function* () {
4747
const commandlet = new SfCommandlet(sfProjectPreconditionChecker, new GetQueryAndApiInputs(), new QueryPlanExecutor());
4848
yield* Effect.promise(() => commandlet.run());
4949
});
50+
51+
export const queryPlanDocument = Effect.fn('sf.data.query.explain.document')(function* () {
52+
const commandlet = new SfCommandlet(
53+
sfProjectPreconditionChecker,
54+
new GetDocumentQueryAndApiInputs(),
55+
new QueryPlanExecutor()
56+
);
57+
yield* Effect.promise(() => commandlet.run());
58+
});

packages/salesforcedx-vscode-soql/src/commands/queryUtils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,35 @@ export class GetQueryAndApiInputs implements ParametersGatherer<QueryAndApiInput
6363
}
6464
}
6565

66+
export class GetDocumentQueryAndApiInputs implements ParametersGatherer<QueryAndApiInputs> {
67+
public async gather(): Promise<CancelResponse | ContinueResponse<QueryAndApiInputs>> {
68+
const editor = vscode.window.activeTextEditor;
69+
if (!editor) {
70+
return { type: 'CANCEL' };
71+
}
72+
73+
const query = editor.document.getText().replaceAll(/(\r\n|\n)/g, ' ').trim();
74+
if (!query) {
75+
return { type: 'CANCEL' };
76+
}
77+
78+
const restApi = {
79+
api: 'REST' as const,
80+
label: nls.localize('REST_API'),
81+
description: nls.localize('REST_API_description')
82+
};
83+
84+
const toolingApi = {
85+
api: 'TOOLING' as const,
86+
label: nls.localize('tooling_API'),
87+
description: nls.localize('tooling_API_description')
88+
};
89+
90+
const selection = await vscode.window.showQuickPick([restApi, toolingApi]);
91+
return selection ? { type: 'CONTINUE', data: { query, api: selection.api } } : { type: 'CANCEL' };
92+
}
93+
}
94+
6695
/** Formats error messages for better user experience */
6796
export const formatErrorMessage = (error: unknown): string => {
6897
let errorString: string;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { CancellationToken, CodeLens, EventEmitter, ExtensionContext, languages, Range, TextDocument } from 'vscode';
9+
import { nls } from '../messages';
10+
import { isDefaultOrgSet, onDefaultOrgChange } from '../services/org';
11+
12+
const SOQL_DOCUMENT_SELECTOR = { language: 'soql' };
13+
14+
const provideCodeLenses = async (document: TextDocument, _token: CancellationToken): Promise<CodeLens[]> => {
15+
if (document.getText().trim().length === 0 || !(await isDefaultOrgSet())) {
16+
return [];
17+
}
18+
return [
19+
new CodeLens(new Range(0, 0, 0, 0), {
20+
command: 'sf.data.query.explain.document',
21+
title: nls.localize('soql_query_plan_codelens'),
22+
tooltip: nls.localize('soql_query_plan_codelens')
23+
})
24+
];
25+
};
26+
27+
export const registerSoqlCodeLensProvider = (context: ExtensionContext): void => {
28+
const changeEmitter = new EventEmitter<void>();
29+
30+
context.subscriptions.push(
31+
languages.registerCodeLensProvider(SOQL_DOCUMENT_SELECTOR, {
32+
onDidChangeCodeLenses: changeEmitter.event,
33+
provideCodeLenses
34+
}),
35+
onDefaultOrgChange(() => changeEmitter.fire()),
36+
changeEmitter
37+
);
38+
};

packages/salesforcedx-vscode-soql/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import * as Effect from 'effect/Effect';
1010
import * as Scope from 'effect/Scope';
1111
import * as vscode from 'vscode';
1212
import { dataQuery } from './commands/dataQuery';
13-
import { queryPlan } from './commands/queryPlan';
13+
import { queryPlan, queryPlanDocument } from './commands/queryPlan';
1414
import { soqlBuilderToggle } from './commands/soqlBuilderToggle';
15+
import { registerSoqlCodeLensProvider } from './commands/soqlCodeLensProvider';
1516
import { soqlOpenNewBuilder, soqlOpenNewTextEditor } from './commands/soqlFileCreate';
1617
import { SOQLEditorProvider } from './editor/soqlEditorProvider';
1718
import { startLanguageClient, stopLanguageClient } from './lspClient/client';
@@ -43,6 +44,7 @@ export const activateEffect = Effect.fn(`activation:${EXTENSION_NAME}`)(function
4344
yield* Effect.sync(() => {
4445
context.subscriptions.push(SOQLEditorProvider.register(context));
4546
QueryDataViewService.register(context);
47+
registerSoqlCodeLensProvider(context);
4648
});
4749

4850
const registerCommand = api.services.registerCommandWithRuntime(getSoqlRuntime());
@@ -62,7 +64,8 @@ export const activateEffect = Effect.fn(`activation:${EXTENSION_NAME}`)(function
6264
),
6365
registerCommand('sf.data.query.input', dataQuery),
6466
registerCommand('sf.data.query.selection', dataQuery),
65-
registerCommand('sf.data.query.explain.selection', queryPlan)
67+
registerCommand('sf.data.query.explain.selection', queryPlan),
68+
registerCommand('sf.data.query.explain.document', queryPlanDocument)
6669
],
6770
{ concurrency: 'unbounded' }
6871
);

packages/salesforcedx-vscode-soql/src/messages/i18n.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export const messages = {
3434
"ERROR: We can't save the file to the specified directory. Make sure you have write permissions for the directory.",
3535
data_query_input_text: 'SFDX: Execute SOQL Query...',
3636
data_query_selection_text: 'SFDX: Execute SOQL Query with Currently Selected Text',
37+
soql_query_plan_codelens: 'Get Query Plan',
38+
query_plan_document_text: 'SFDX: Get SOQL Query Plan with Current File',
3739
data_query_success_message: 'Query executed successfully. Found %d records. Results saved to: %s',
3840
data_query_error_message: 'Error executing query: %s',
3941
data_query_error_org_expired:

packages/salesforcedx-vscode-soql/src/services/org.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import type { Connection } from '@salesforce/core';
99
import { getServicesApi } from '@salesforce/effect-ext-utils';
1010
import * as Effect from 'effect/Effect';
11+
import * as Stream from 'effect/Stream';
1112
import * as SubscriptionRef from 'effect/SubscriptionRef';
13+
import * as vscode from 'vscode';
1214
import { getSoqlRuntime } from './extensionProvider';
1315

1416
/** TargetOrgRef (getDefaultOrgRef) has no requirements */
@@ -23,6 +25,23 @@ export const isDefaultOrgSet = (): Promise<boolean> =>
2325
})
2426
);
2527

28+
/**
29+
* Calls `listener` whenever the default org changes. Returns a `Disposable` for cleanup.
30+
* Skips the initial current value — only fires on subsequent changes.
31+
*/
32+
export const onDefaultOrgChange = (listener: () => void): vscode.Disposable => {
33+
const abortController = new AbortController();
34+
void getSoqlRuntime().runPromise(
35+
Effect.gen(function* () {
36+
const api = yield* getServicesApi;
37+
const ref = yield* api.services.TargetOrgRef();
38+
yield* ref.changes.pipe(Stream.runForEach(() => Effect.sync(listener)));
39+
}),
40+
{ signal: abortController.signal }
41+
);
42+
return new vscode.Disposable(() => abortController.abort());
43+
};
44+
2645
export const getConnection = (): Promise<Connection> =>
2746
getSoqlRuntime().runPromise(
2847
Effect.gen(function* () {

0 commit comments

Comments
 (0)