Editing tools for WEML (White Estate Markup Language) documents in the side bar (the
WEML icon in the Activity Bar): one WEML view made of panes that behave like the Explorer's
Outline and Timeline: each collapses from its header, each open one scrolls on its own, the
sash between two open panes resizes them (double-click it to share the height evenly again),
and collapsed panes and sizes are remembered.
Tools — everything that edits the document:
- Wrap or insert — one searchable list of tags, grouped as the WEML docs are (Containers,
Blocks, Inlines): type to filter, ↑ ↓ and Enter (or a click) to choose, then the button in the
field applies it — again as often as needed. An inline —
w-format (each text-formatting
value), w-color, w-lang, w-non-egw, w-entity, w-sent, a (link) — wraps the
selection (required attributes become tab stops); every container (w-para, w-heading,
w-para-group, w-toc, and a new div section with any of them or with a w-page page break), block
(w-text-block, table, w-list, figure, hr) and empty inline (w-note, w-page, br,
the a id anchor) inserts a snippet at the cursor. The chosen tag is remembered;
- Document — Renumber div ids (1…n): every
<div> id becomes its position in document
order (also WEML Tools: Renumber <div> ids in the Command Palette);
- Hotkeys in
.weml editors — Ctrl+B / Ctrl+I / Ctrl+U wrap the selection in
<w-format type="bold|italic|underline">;
Search Suggestions — EGW search: a query and a language,
then per result its ParaId and BookCode (copyable), its title and, for a paragraph, a ready
<a href="egw://book|bible/…"> that can be copied or inserted in place of the selection
(the selection becomes its label; # without one). Needs sign-in: until then — or until
the settings are filled in — this section offers that instead of the search.
The tools act on the active WEML editor — the last text editor in WEML language mode, or the
only visible one. Clicking in the side bar does not lose it.
It is one view on purpose. VS Code merges a container's only view into the container's title
bar, so the view's buttons — the account button, and Copy Redirect URI and Settings in the
… menu — sit in the WEML header itself; extensions have no other way there
(viewContainer/title is proposed API). A feature to come is one more pane in this view, not a
view of its own, so it shares that header and the account.
The view's content is the shared WEML panel (weml-monaco/panel) — the same Tools and
Search Suggestions a web editor built on weml-monaco shows. Here it runs in the webview
(webview-src/main.ts, bundled into dist/webview/panel.js) and only presents: it calls the
extension host (src/views/protocol.ts, wemlView.ts) for every edit and every search, so the
sign-in, the token and the requests never leave the extension.
Sign-in
Sign-in is OpenID Connect — authorization code with PKCE, optionally through the server's own
login page — on VS Code's own machinery:
- the account button in the WEML header, just before the
… — one account for the
whole extension and every group in it. It signs in when signed out; signed in, it opens the
account menu: the Gravatar with the name and email, Open Account Page (only when
wemlTools.auth.accountPageUrl is set), Sign Out, Copy Redirect URI and Settings. VS Code only draws
codicons in a title bar, so the Gravatar is in the menu rather than on the button, and the menu
is a QuickPick, which always has a filter box. Signing out also clears the search results
found under the old session;
- EGW Writings is also a VS Code authentication provider: the account appears in the
Accounts menu (bottom of the Activity Bar) and can be signed out from there too;
- the authorization response is checked against the request (
state) and, per RFC 9207,
against the server (iss must equal the discovered issuer; a server that advertises
authorization_response_iss_parameter_supported must send it) — a mix-up attack through
another server is refused before any code is exchanged;
- tokens are kept in VS Code's SecretStorage (the OS keychain), shared by all windows, and
the access token is refreshed with the refresh token before it expires (
offline_access);
- the browser comes back to VS Code through its URI handler (see Redirect URI below), or
through a one-shot local listener when the server only allows an
http://localhost redirect.
Every setting's description ends with an Edit in settings.json link.
Nothing environment-specific is built in. Everything comes from the settings below, which are
machine-scoped: they can be set in the user settings only (Ctrl+, → User, or
settings.json), never in a workspace's .vscode/settings.json, so they cannot be committed.
| Setting |
Notes |
wemlTools.auth.authority |
OIDC issuer; discovery is read from it |
wemlTools.auth.clientId |
must allow the redirect URI |
wemlTools.auth.scopes |
include offline_access, and search for suggestions |
wemlTools.auth.loginPageUrl |
optional; the authorize request is passed to it as next |
wemlTools.auth.redirectUri |
empty (VS Code's URI, see below) or http://localhost:… |
wemlTools.auth.accountPageUrl |
optional full URL of the user's page; shows Open Account Page |
wemlTools.search.apiUrl |
GET <apiUrl>/search/suggestions?query=&lang= |
wemlTools.search.defaultLanguage |
en by default |
// User settings.json
"wemlTools.auth.authority": "https://…",
"wemlTools.auth.clientId": "…",
"wemlTools.auth.scopes": "openid offline_access … search",
"wemlTools.auth.loginPageUrl": "https://…",
"wemlTools.search.apiUrl": "https://…"
The browser tab of the sign-in stays open afterwards: a page may only close a tab its own
script opened, and this one was opened by the OS and went through several pages. With a
vscode:// redirect the tab is left on the server's last page; with an http://localhost one
the extension's own page tries window.close() and otherwise says the tab can be closed.
VS Code's built-in GitHub and Microsoft sign-ins leave theirs open the same way.
Redirect URI
After sign-in the server sends the browser back to a redirect URI, and it only does so to a
URI registered with the OIDC client — so this is the one thing to set up on the server.
Leave wemlTools.auth.redirectUri empty (recommended). The browser then comes back to
VS Code itself, through the URI VS Code assigns to this extension. That URI depends on where
VS Code runs, and every environment you sign in from needs its own entry on the server:
| VS Code |
Redirect URI |
| VS Code (desktop) |
vscode://egwwritings.weml-tools/auth-callback |
| VS Code Insiders |
vscode-insiders://egwwritings.weml-tools/auth-callback |
| In the browser: vscode.dev, Codespaces, a remote server opened in the browser |
an https URL of that VS Code (e.g. https://vscode.dev/callback?…), produced by vscode.env.asExternalUri |
Register the URI exactly as shown — all lower case, no query: servers such as OpenIddict
compare redirect URIs character by character, so vscode://EGWWritings.weml-tools/… or
…/auth-callback?windowId=1 is a different URI to them. That is also why the desktop URI is
not taken from vscode.env.asExternalUri, which appends ?windowId=…; the callback then goes
to the last active VS Code window — the one signing in.
The scheme is VS Code's own (vscode.env.uriScheme), so a fork of VS Code has its own one too.
Rather than working it out, run WEML Tools: Copy Redirect URI (Command Palette, or the
view's … menu): it copies the exact URI the current window will send, ready to register.
In a browser-based VS Code check that URI before registering it — it may carry query
parameters a server that matches redirect URIs exactly will not accept.
Set redirectUri to an http://localhost:<port>/<path> URL only if the server refuses
custom-scheme URIs like vscode:// and accepts loopback ones (RFC 8252). The extension then
listens on that port for the moment of the redirect, so the port must be free while signing in.
The same URL works in VS Code and Insiders alike, but not in a browser-based VS Code.
Changing the authority, client id or scopes drops the session — the token was issued for the
old ones — and the view asks to sign in again.
For features to come
Sign-in is not part of the search: it is a shared layer every feature uses, and nothing else
touches tokens.
In this extension, a feature takes the EgwApi (src/auth/egwApi.ts) and calls
api.fetch(url, init): it adds Authorization: Bearer …, refreshes the token before it
expires, and on a 401 refreshes once more and retries. Signed out, it throws
NotSignedInError without sending anything. The feature's API base URL is its own setting,
wemlTools.<feature>.apiUrl, machine-scoped like the others; a scope it needs is added to
wemlTools.auth.scopes.
The other extensions of the WEML set (WEML Language Support, WEML Preview — none of them
signs in yet) get the same object as this extension's exported API, once they list
EGWWritings.weml-tools in their extensionDependencies:
const egw = await vscode.extensions.getExtension('EGWWritings.weml-tools')?.activate();
const response = await egw.fetch(`${apiUrl}/…`);
egw.onDidChangeSession(() => …);
or, without depending on this extension's code, through VS Code itself:
vscode.authentication.getSession('egw-oidc', [], { createIfNone: true }).
Requires
WEML Language Support (EGWWritings.weml-support), which owns
the weml language mode; it is declared in extensionDependencies.
Relation to WEML Language Support
This is the toolbar that WEML Language Support used to have in the bottom panel (the WEML
tab), moved to the side bar together with the rest of the editing tools — Renumber div ids
and the Ctrl+B / Ctrl+I / Ctrl+U hotkeys. WEML Language Support keeps the language
features and formatting (Format Document, Alt+Shift+F).
Shared code
The language logic comes from the workspace package
weml-language-core (bundled in): the schema, structure rules and
snippets, wrap / divRenumber, the tool model — the tags this view's
picker offers, the same ones weml-monaco's toolbar and the web editor offer — and the EGW search
client with the egw:// link builder (src/search/ there; its suggestionLinks.ts holds the
list of Bible book ids that decides between egw://bible/… and egw://book/…). This extension
keeps what is VS Code's: the sign-in, the token, and the search request made with it.
Developing
npm install # at the repository root (npm workspaces)
npm run build # dist/extension.js + dist/webview/ (the panel, from weml-monaco's sources)
npm run watch
npm run check-types # the extension and the webview script
npm run package # vsce package --no-dependencies
The sign-in protocol (PKCE, the authorize URL, the RFC 9207 check, the token requests) is
weml-language-core's auth/oidc.ts, tested there; this extension keeps the VS Code side of it
(the authentication provider, SecretStorage, the redirect callbacks).
F5 (Run WEML Tools, with this folder open) starts an Extension Development Host on
../../samples.
The Activity Bar icon media/weml.svg is the WEML Editor's logo
(weml-storybook/src/modules/WemlEditor/icons/vscLogoIconJson.ts) as a plain
currentColor SVG.
License
MIT