Nerdulator — VS Code extension
Select text in the editor, right click, choose Nerdulate Selection, and see
everything deterministically derivable from it in the side bar.
Version 0.1.0. VS Code 1.85 or later. A port of the Nerdulator Chrome extension
0.1.0, and a sibling to it rather than a fork: the two render the same shape
from the same API and should never disagree about what an input means.
Installing it
For development:
- Open this folder in VS Code
- Press
F5 (the Run Nerdulator launch configuration)
- In the window that opens, select some text, right click, Nerdulate Selection
There is no build step and no runtime dependency. npm install is only needed
for npm run typecheck and npm run package; the extension itself runs
straight from source.
To install a build into your own VS Code:
npm install
npm run package
code --install-extension nerdulator-0.1.0.vsix
Using it
|
|
| Right click a selection |
Nerdulate Selection |
Ctrl+Alt+N / Cmd+Alt+N |
the same, on whatever is selected |
| Command palette |
Nerdulator: Nerdulate Something, which asks |
| The Nerdulator icon in the Activity Bar |
the panel, with an input box |
‹ in the panel's title bar |
back, one step through the walk |
↗ in the panel's title bar |
the current reading on nerdulator.com |
The keybinding and the palette command work with no selection: they fall back to
the word under the cursor, because "select it first" is an unhelpful answer to
someone whose cursor is already sitting on the UUID they want. The context menu
entry is gated on editorHasSelection, so that path always has a real
selection. Multiple cursors are all inspected, joined by newlines, which is what
a copy would have produced.
The one architectural rule
The extension contains no Nerdulator intelligence. It does not know what a
UUID is, how to balance an equation or which of three readings of "1066" is
best. It sends text to https://nerdulator.com/api/v1 and renders whatever
comes back.
website ─┐
Chrome extension ┤
├─→ deterministic core (22 engines)
this extension ┘ ↑
└── one implementation, one set of answers
There is exactly one place that decides what an input means. An engine added to
the site appears here with no change to this code. If you find yourself writing
if (reading === 'uuid'), something has gone wrong.
src/api/nerdulator.js is the only file that makes a request. Nothing else
knows the API exists.
What renders
The API returns one shape whatever the subject, so this renders that shape
rather than the subject:
readings[] one per interpretation, best first
name, reading "Number", "integer"
match exact | normalised | fallback
summary a sentence
url the full page on nerdulator.com
facts[] label, value, value_type, note?, approximate?
relations[] relation?, label, target{ reading, canonical, value, url }
A fact whose value points at another object is itself clickable, and the
remaining edges, neighbouring values and cross-readings, sit under a heading
below. Clicking either inspects the target, which is how the graph is walked.
The API reports a linked fact twice, once in facts and once in relations,
because they are the same edge seen from two sides. Rendering both put "Next
major 1.0.0" in a row and again as a chip directly beneath it, which reads as a
bug even though the data is correct. The fact's value becomes the link and the
duplicate chip is dropped, so the thing you click is the thing you are reading.
Things done deliberately
match: "fallback" is shown, and labelled. The word engine returns a
character count for almost any string, so "flibbertigibbet" does get an answer.
That is a real result and is displayed, but saying "Not recognised. Inspected
as plain text" is the honest framing, because the site did not recognise it, it
counted the characters. It is a 200, not an error.
Several readings are presented as all correct. "1066" is genuinely an
integer, a year and a colour. The panel says "3 readings. All of them are
correct" rather than picking one, because picking is exactly what the product
does not do.
engine is never shown. It is an internal grouping, "reference",
"counting", "linear", and means nothing to a reader. The public reading id is
shown instead, and that one is stable.
value_type only decides alignment. Numbers are right-aligned and prose is
not. It is never used to reformat a value: the API already rendered it.
Fact labels are treated as prose. The API is explicit that label and
note are written for people and get reworded. Nothing here keys off them.
What is different from the Chrome version, and why
The rendering is a port and reads almost line for line. Everything around it
moved, because the two hosts are shaped differently in four ways that matter.
1. The host fetches, not the panel
In Chrome the side panel fetched for itself: an extension page is an ordinary
origin, the API sends Access-Control-Allow-Origin: *, and the service worker
had no reason to be involved.
Here the request is made by the extension host, in Node. That has three
consequences, all of them improvements:
- The webview needs no network capability at all. Its content security
policy has no
connect-src, so it could not make a request if something in it
tried. Every byte it renders arrived by postMessage. That is the analogue of
the Chrome version shipping with no host permission, and it is stronger,
because there the panel really could fetch and simply chose not to need to.
- There is no CORS involved, because Node has no origin.
- The API client is callable from anywhere in the extension, not only from
the surface that happens to be visible.
2. The host owns the state
A Chrome side panel persists while it is open, so it kept its own history and
current. A VS Code webview view is destroyed as soon as it is hidden,
collapsed, or dragged into another container.
So the host holds the walk and the webview is a pure renderer that can be thrown
away and rebuilt from one message. retainContextWhenHidden is deliberately
not set: it would keep a hidden DOM alive at a real memory cost to buy back
state the host already has.
3. The panel is the editor's colour, not the site's
The Chrome panel defined a light and a dark palette of its own and chose with
prefers-color-scheme, because a browser side panel has no theme to belong to.
This one sits in the side bar next to the explorer, where brand teal against
somebody's Solarized or high contrast theme looks like a foreign object. Every
colour in media/main.css is a --vscode-* variable, so the panel is whatever
theme the reader chose and high contrast works without a special case.
The teal survives in the marketplace icon. The Activity Bar icon is a flat
monochrome SVG because VS Code tints it by using the shape as a mask: a colour
there would be discarded and a gradient would come out as one solid block.
Back and Open in Nerdulator are view title actions, where VS Code puts view
actions, driven by the nerdulator.canGoBack context key. The view's own title
bar already says NERDULATOR, so a second header would repeat the name and take a
line from a surface that is usually too narrow already.
ETag revalidation, which the browser did for free
Relations use GET /v1/objects/{reading}/{canonical} rather than posting to
/inspect again, for the reasons the API was designed around:
- A POST is not cacheable by anything. Every step of a walk would reach the
server.
- A revalidated request is refunded against the rate limit, so walking back
through the graph costs nothing.
- The target already carries
reading and canonical, the two path segments.
In Chrome, points 1 and 2 arrived with no code at all: the panel called fetch
and the browser's HTTP cache answered a repeat visit. Node's fetch has no
cache. Ported naively, the one property the endpoint exists for would have
quietly stopped happening, and every step back through the graph would have
re-run an inspection on a mobile-class CPU.
So src/api/nerdulator.js keeps the smallest thing that restores it: the ETag
and the body of each GET, an If-None-Match header, and the stored body
returned on a 304. Bounded to 200 entries, memory only, never written to disk
because the keys are things somebody looked at, and cleared when the window
closes.
It revalidates rather than holding a copy for a while, and that distinction
is the point. Answers are deterministic within one behaviour_version, but a
deploy changes that version. Revalidating leaves the server as the one that
decides whether the old bytes are still the right bytes; a plain time-to-live
would serve last week's answer with no way to know it was stale.
POST /inspect is never cached. A POST response is not addressable, so there is
nothing to key on.
Two things that are easy to get wrong
The panel may not exist when a command fires. Revealing the view and the
view announcing itself are separate events, and the command that caused the
reveal returns between them. A request landing in that gap has nowhere to
render. NerdulatorViewProvider holds it in pending and the webview's ready
handshake flushes it. This is the same race the Chrome version solved with a
session-storage write and a timestamp; the shape differs, the problem does not.
The reveal is executeCommand('nerdulator.panel.focus') rather than
view.show(), because show() needs a resolved view and the first call happens
when the container has never been opened. VS Code generates <viewId>.focus for
every contributed view and it works in both cases.
Two quick clicks can come back in either order. Relation chips stay
clickable while a request is in flight, and nothing guarantees the answers
arrive in the order the questions went out. Without a guard the slower one wins
and the panel shows something nobody asked for. Every request carries a
generation number and a stale answer is dropped. Stepping back bumps it too, so
an in-flight request cannot land on top of the page the reader just returned to.
Permissions and privacy
VS Code extensions are not permission-scoped the way a Chrome extension is, so
the honest statement is what this one touches:
- It reads the current selection, or the word under the cursor, and only
when you invoke a command.
- It makes one HTTPS request to
nerdulator.com per inspection. The API takes
no credentials and sets no cookies.
- It declares
untrustedWorkspaces: supported and virtualWorkspaces: true,
because it never executes workspace code and never touches the file system.
- No telemetry, no settings, no storage. The ETag cache is in memory and goes
when the window closes.
- The API's own usage logging records the shape of a request, never the
input: the recognised reading ids and the input length. Requests from here
carry
User-Agent: nerdulator-vscode/0.1.0 and nothing else identifying.
What you inspect leaves your machine. That is the whole mechanism, and it is
worth stating plainly in an editor, where a selection is more likely to be
somebody's own source than a word on a web page.
Security
The selected text came from a file, and the values came back through an API.
Neither is trusted.
- Every node in the webview is built with
createElement and filled with
textContent. There is no innerHTML in this codebase, and adding one would
turn a selected word into script.
- The webview runs under
default-src 'none' with a per-load nonce, and
localResourceRoots limited to media/.
- No
connect-src, so the webview cannot reach the network.
- HTTPS only, no remote code, no
eval, no dynamic execution.
Files
package.json commands, menus, the view container, keybinding
src/
extension.js activation; works out what text the reader meant
api/nerdulator.js the only file that talks to the API, plus the ETag cache
panel/view.js the webview view: state, messaging, the HTML shell
media/
main.js renders the shape; no per-reading branches, no fetching
main.css every colour is a --vscode-* variable
icons/
activity.svg monochrome, because VS Code masks it
icon128.png the marketplace icon
test/
panel.test.js the panel and the renderer, no editor involved
stubs.js just enough `vscode` to run view.js outside VS Code
integration/ the same extension inside a real VS Code
No framework and no build step. It renders a few dozen deterministic facts and
does not need one. npm run typecheck runs tsc over the JSDoc with strict
and noUncheckedIndexedAccess, matching the site's own settings, without
introducing a compile.
Testing
The Chrome extension has no tests. These exist because the port is not the
rendering, which was a transcription, but the ordering rules around it, and
those were wrong on the first attempt.
npm test # the panel and the renderer
npm run test:vscode # inside a real VS Code
xvfb-run -a npm run test:vscode # the same, headless
VSCODE_EXECUTABLE=/usr/share/code/code npm run test:vscode # reuse an install
npm test runs against the live API, not a fixture. The extension's whole
contract is "render whatever the API returns", so a fixture would only prove
that it renders a copy of what the API said on the day somebody captured it. A
test that goes over the wire fails when the envelope moves, which is the news
worth having.
The two suites answer different questions, and neither can answer the
other's. test/stubs.js fakes enough of the vscode module to run
src/panel/view.js in plain Node, which makes the ordering rules cheap to test:
a request arriving before the panel exists, two answers racing, back with
nothing to go back to. But a stub will happily agree with a manifest VS Code
would reject, so test/integration launches a real editor and checks that the
view container really generated nerdulator.panel.focus, that opening the view
activates the extension, and that the commands exist.
One thing neither proves on its own: whether VS Code loads media/main.js under
that content security policy. NERDULATOR_SCREENSHOT=1 dumps the running window
so it can be looked at, because if the nonce or the policy were wrong the script
would be blocked, ready would never arrive, and the panel would sit on its
hint text with the request still pending.
What they caught
A superseded answer landed on top of the page the reader had gone back to.
The generation guard claimed its number inside execute(), which runs after
the await on revealing the view. A request that had been revealed but not yet
started therefore took a fresh generation after back() had already tried to
invalidate it. The guard looked right and did nothing in the one case it was
written for. It now claims the number in run(), synchronously, when the
request is asked for. Lesson: a sequence number that is claimed after an await
is not a sequence number.
Known limits
- Selections over 10 KB are refused before a request is made; the API's own
limit is the same.
- Rate limits are 5 requests a second, 20 burst, 1,000 an hour per caller,
shared with anything else calling the API from your address.
- A request that has not answered in 10 seconds is abandoned. Node, unlike a
browser, will wait indefinitely otherwise.
- Behind a corporate proxy, the host's
fetch follows VS Code's own proxy
settings (http.proxy, http.proxySupport). If those are wrong, every
inspection reports "Nerdulator could not be reached".
- The panel is a side bar view, so it is narrow. Long fact values wrap; long
relation targets are ellipsised with the full text on hover.
Not in 0.1.0
A hover provider, a CodeLens, diagnostics, "find the 7 nerdy things in this
file", a status bar entry, history, settings, an API base override, accounts or
keys. Every one of those is a decision about when to interrupt somebody's
editing, and none of them is answerable before anyone has used the thing. The
job is: select something, nerdulate it.
Publishing
MIT, and the publisher id nerdulator on both the VS Code Marketplace and Open
VSX. The full sequence, including the two logins that have to be done by a
person, is in PUBLISHING.md.