DEV Community

Cover image for Virga: run anonymous Apex and SOQL from a VS Code tab with Ctrl+S
Wiktor Małyska
Wiktor Małyska

Posted on

Virga: run anonymous Apex and SOQL from a VS Code tab with Ctrl+S

Most of the Apex I run by hand is throwaway. A few lines to check a query, a System.debug to see what a method returns, a quick look at some records. I wanted that to be one key press in VS Code, with the result readable at a glance and nothing left behind in the project afterwards.

So I built Virga, a free, MIT-licensed VS Code extension for Apex developers, and the first version is out. This post covers what it does and a few decisions behind it.

An anonymous Apex scratch run with Ctrl+S, with debug lines, levels and limits in color in the Virga output

Install

You need VS Code 1.90 or newer, the Salesforce CLI (sf) on your PATH with an authorized org set as the project's default, and a Salesforce DX project. The Salesforce Extension Pack is recommended. It brings highlighting and the language servers that work inside scratches, and the local type completion, the signature checks and the snippets attach to its apex language, so those three need it. Scratches run without it, which is why Virga doesn't declare the pack as a dependency.

A scratch, from click to result

In a folder with sfdx-project.json, Virga adds two buttons to the status bar: ▷ Apex and SOQL. Each click opens a new tab (scratch-1.apex, scratch-2.apex, scratch-1.soql, ...). The script in the screenshot above is this:

List<Account> accounts = [
    SELECT Id, Name, Industry FROM Account
    WHERE Industry != null ORDER BY Name LIMIT 3
];
System.debug(accounts.size() + ' accounts with an industry');
for (Account a : accounts) {
    System.debug(LoggingLevel.INFO, a);
}
System.debug(LoggingLevel.WARN, Limits.getQueries() + ' of 100 queries used');
Enter fullscreen mode Exit fullscreen mode

Press Ctrl+S (Cmd+S on macOS). An .apex scratch goes to sf apex run, a .soql scratch to sf data query. The org is the one sf itself would pick: the SF_TARGET_ORG environment variable, then target-org in the project's .sf/config.json, then your global config. Virga passes it to sf as --target-org, so the header of every result names the org the code really ran in.

The result goes to the Virga output channel. For Apex that means ✔ Compiled ✔ Executed, every debug line with its line number and log level, and the limits the run used. When something goes wrong, the output points at it. Here is a run that divides by zero:

System.debug('before');
Integer z = 0;
Integer i = 1 / z;
Enter fullscreen mode Exit fullscreen mode
✔ Compiled  ✖ Exception: System.MathException: Divide by 0
  at AnonymousBlock: line 3, column 1

  3 │ Integer i = 1 / z;
    │ ^

  [line 1] DEBUG  before

  Limits  SOQL queries 0/100 · query rows 0/50000 · DML statements 0/150 · DML rows 0/10000
          CPU time 0/10000 ms · heap 0/10000000 bytes
Enter fullscreen mode Exit fullscreen mode

A SOQL scratch prints the record count and a table. Relationship fields are flattened to dotted names, subqueries show as [2 rows], and long values are cut with …:

SELECT Id, Name, Account.Name, Account.Owner.Name, Email FROM Contact ORDER BY Name LIMIT 4
Enter fullscreen mode Exit fullscreen mode
✔ 4 records
┌────────────────────┬──────────────┬────────────────────────┬────────────────────┬───────────────────────┐
│ Id                 │ Name         │ Account.Name           │ Account.Owner.Name │ Email                 │
├────────────────────┼──────────────┼────────────────────────┼────────────────────┼───────────────────────┤
│ 003Dm0000AbC011AAA │ Andy Young   │ Dickenson plc          │ OrgFarm EPIC       │ a_young@exampleco.com │
│ 003Dm0000AbC013AAA │ Arthur Song  │ United Oil & Gas Corp. │ OrgFarm EPIC       │ asong@abc.com         │
│ 003Dm0000AbC015AAA │ Ashley James │ United Oil & Gas, UK   │ OrgFarm EPIC       │ ajames@abc.com        │
│ 003Dm0000AbC017AAA │ Avi Green    │ United Oil & Gas Corp. │ OrgFarm EPIC       │ agreen@abc.com        │
└────────────────────┴──────────────┴────────────────────────┴────────────────────┴───────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Columns come from the SELECT list as well as from the records, so a relationship that is null in every row still keeps its own dotted columns.

What runs is what you see

A run uses exactly the text in the editor at the moment of Ctrl+S. Virga writes a copy of it to a temporary folder and hands that to sf, never the scratch file itself and never on the command line. Keys you type after Ctrl+S are not part of that run.

Saving never runs a scratch. Auto save doesn't, and Save All from another file doesn't either. Only Ctrl+S in the scratch, the ▶ button in its editor title, or Virga: Run Scratch does. A second Ctrl+S during a run is queued, and a newer Ctrl+S in the same tab replaces the queued text.

Closing without "save changes?"

Closing a scratch tab discards it, with no dialog. The trick behind it is small. Virga sets a short auto save, afterDelay with 100 ms, as the language default for apex-anon and soql. With a delay that short, VS Code treats the document as being saved: no dirty dot on the tab, and closing it never asks. If a background save fails because something else has the file locked, Virga keeps retrying it, with a pause that grows up to 5 seconds, until it goes through.

Since the run uses its own copy, a run saves nothing, and no format on save or whitespace trimming ever happens because of Ctrl+S.

The cost is that VS Code lets an extension set this default only per language, so other apex-anon files (for example scripts/apex/hello.apex) save themselves 100 ms after a change too. Classes and triggers are not affected. To switch it off:

{
  "[apex-anon]": { "files.autoSave": "off" },
  "[soql]": { "files.autoSave": "off" }
}
Enter fullscreen mode Exit fullscreen mode

With that, closing a scratch with unsaved text asks again. Whatever you answer, the scratch file is deleted.

A scratch is a real file in .sfdx/virga-scratch/, which Salesforce DX projects already ignore in git. The language servers of the Extension Pack therefore complete and underline it like any other file. Tabs that VS Code restores after a restart are closed at startup, and two windows on the same project never delete each other's scratches.

If you use the IntelliJ IDEA keymap extension, where Ctrl+S is Save All, Virga's Ctrl+S still wins inside scratch editors, and only there.

Three smaller things for regular Apex files

Completion for types declared in the same file. The Apex language server doesn't offer an inner class, interface or enum by its simple name, even when it's declared a few lines below. Virga adds the types declared in the current file, inner ones at any depth, with a detail such as inner class of OrderIntake. It stays out of the way after a dot, in comments and strings, after @, and inside inline SOQL.

Signatures that break completion. A raw List in an @InvocableMethod parameter (and a few related shapes) makes the language server throw internally and stop suggesting members after . in the whole file. All you see is a syntax error. Virga marks the real cause and offers a quick fix that types the collection from the classes in your file. I measured which shapes do this on 274 signatures, and I'll write that up in a separate post.

Snippets the Extension Pack doesn't have. Annotations (type @, then Inv, and get InvocableMethod(...)), keywords and modifiers such as return, else if and with sharing, DML statements, and user mode: insert as user, Database.queryWithBinds with AccessLevel.USER_MODE, WITH USER_MODE. No Virga prefix is one the pack already uses, so a suggestion list never shows the same name twice.

Privacy

Virga has no telemetry and no network code. Runs call your local sf against the org you chose. sf has usage telemetry of its own, so Virga starts every run with SF_DISABLE_TELEMETRY=true. To turn it off for your own sf commands too:

sf config set disable-telemetry true --global
Enter fullscreen mode Exit fullscreen mode

Why "Virga"

Virga is rain that evaporates before it reaches the ground. The code you try in a scratch never lands in your project either.

Feedback and contributions

The code is at github.com/wiktormalyska/virga. It has no runtime dependencies and no build step, and npm test runs the unit tests in plain Node. Issues and small pull requests with a test are welcome. If something behaves differently on your setup than described here, an issue with the Virga output attached helps most.

Salesforce, Apex and related marks are trademarks of Salesforce, Inc. Virga is an independent project and is not affiliated with or endorsed by Salesforce.

Top comments (0)