32. Editors
tcm lsp is a language server (LSP over stdio) that runs alongside TypeScript's own. TypeScript keeps doing
completions, type errors, go-to-definition and refactors; tcm adds what only it knows. It works on scratch files:
the .ts and .tsx files at the top of the codebase directory, the ones the watcher checks.
32.1 What it shows
- What each definition is to the codebase, as a code lens above it, updated as you type (unsaved buffers
are analyzed in memory):
+ new lib.squarewith an Add to codebase action;~ update lib.square (was #a1b2c3d4)with Update codebase;lib.square #a1b2c3d4 · runs on the serveronce it's saved (placement shown for app code);- aliases and conflicts;
tcm: 2 type errors; nothing hashed until they're fixedwhile TypeScript reports errors.
- Watch values inline, as inlay hints at the end of the line:
square(12) ⧩ 144. They're cached by hash: while you type, values already known for an expression show at once; new ones are evaluated when the file is opened or saved, so a half-typed expression with side effects doesn't run on every keystroke. - Test results on test definitions:
✓ passed (cached)or✗ failed: …, run on open and save like watches. - Hovers on codebase names, in scratch files: every path a definition has, its hash, signature, where it runs and why, how many named definitions use it, and its documentation.
- Debugging with breakpoints in scratch files and definitions: Debug lenses on tests and runnable definitions, and on apps (server and browser at once) (debugging).
- Formatting on save with oxfmt, and Format Document (formatting).
- oxlint's findings as diagnostics (source
oxlint, linked to the rule's page; see linting). - tcm's own errors as diagnostics (source
tcm), such as a top-levelvarordeclarein a scratch file.
Commands, which editors expose as code-lens actions or a command palette:
| Command | Arguments | |
|---|---|---|
tcm.add | file URI | tcm add the file. The file must be saved (tcm saves what's on disk) |
tcm.update | file URI | tcm update from the file |
tcm.mv | from, to | tcm mv |
Each reports its output as a message, and every open file's status is refreshed.
32.2 The codebase as documents
The code lives in the codebase, not in files, so the server also shows the codebase itself.
- Definitions are documents.
.tcm/src/lib/square.tsislib.squarerendered with current names (whattcm viewprints), with a header line giving its names, hash and where it runs (.tsxfor definitions with JSX). They're real files, so debuggers stop in them, and TypeScript reads them as the project does. They're the codebase's own branch's (the onetcm switchchecks out; a session's are in its own directory,.tcm/sessions/<branch>/src, and the language server run with itsTCM_BRANCHopens those). They're read-only (the extension marks.tcm/srcand sessions' views so): to change one, Edit writes it into the scratch file (scratch.ts, orscratch.tsxfor JSX), added to what's there if it holds work (what's in it saved as it is goes), where TypeScript checks it and the lens offers Update codebase. Imports are folded out of sight (the extension setseditor.foldingImportsByDefaultfor TypeScript). - Quick fixes in scratch files (Cmd/Ctrl+.): a codebase name the file doesn't import yet gets Import x from
"#codebase/…" (one per namespace that has it), and a name it imports from the codebase gets Edit x here
too, which adds that definition to the file. Clients can also ask for a definition as a
tcm:/lib/square.tsdocument, with the same text. - Go to Definition (Cmd/Ctrl+click) goes to the source. On a codebase name in a scratch file it opens that
definition's document; in a definition's document every reference is a link, since tcm knows exactly where
each one sits in the rendered text. (In VS Code, the extension's TypeScript plugin hides TypeScript's own
answer, the generated module in
.tcm/cache, so the source is the only result. Other editors show both.) - Find All References lists every definition in the codebase that uses one, at the exact spot, from its dependents. In scratch files these are listed alongside TypeScript's own results.
- Lenses on a definition's document: how many named definitions use it (click to list them), Edit, Rename.
- Symbols. Workspace symbol search (Cmd/Ctrl+T) covers every name in the codebase; the outline shows a document's definitions.
- Hovers in these documents describe whatever is under the cursor, as in scratch files.
For clients that want to show more, the server answers:
tcm/tree- one level of the namespace tree: each definition's kind, role (app, type, table, schedule, server, browser, shared, test), signature, placement, whether it's an endpoint, test status
tcm/apps- each app with its endpoints, browser code, tables (with row counts), schedules and tests
tcm/data- tables, with row counts, rows of older row types, the declaring definition and the row type's fields
tcm/rows- a page of a table's rows for a viewer: searched, sorted by a field and paged in SQLite; the row type's fields (columns), and each row's cells, full value, and whether it's of an older row type
tcm/source- a
tcm:document's text: a definition,tcm:/.data/<table>.txt, ortcm:/.data/<table>/<id>.txt tcm/findByType- type search, as
tcm find ':…' tcm/page- a definition's page: rendered doc (links as
tcm-page:<hash>), highlighted source with references (and imports) as links, placement, uses and used-by tcm/namespace- a namespace's page: its README rendered, and its definitions by role with doc summaries
tcm/find- the finder: names, doc words and types
tcm/branches,tcm/history,tcm/historyEntry,tcm/branchChanges- the Changes view: the branches (how many names differ from this one's), this branch's history entries, and the names an entry changed or another branch has, an edit with its documents before and after
tcm/searchCode- find in files for the codebase: the lines of definitions (as their documents read) and pages with some text, by definition: as written, or a regular expression written
/…/(finding code);caseSensitive,wholeWord,regex(all the text a regular expression),under
It sends tcm/changed when the codebase changes and tcm/dataChanged when the data store does, here or from a
terminal or a running app. tcm.dataMigrate <table> is a command, like tcm.add.
32.3 Pages
As in Unison's codebase UI, definitions and namespaces have pages. Each opens in its own editor tab, named after it (opening one that's already open focuses its tab), and links in a page open more. Open pages refresh when the codebase changes.
- A definition's page: its names, hash, kind and badges (where it runs, endpoint, test result), why it runs
where it does, its doc rendered (links, embeds, examples with their values), its source
highlighted with every reference a link, in its own colour and underlined on hover (its imports too: each name to its definition,
#codebase/…to the namespace's page, a package to its page), and lists of what it uses, what it's used by, and the packages it imports. Buttons: Source (thetcm:document), Edit, Usages, Run (Start for an app:tcm dev, as the Apps view's ▶, or opens it if it's running), Debug, Rename, Copy (the hash). - A file's page: its format, then its content: a Markdown page rendered (its
{@link}s and links to other pages open their pages), an image shown, other text as it is (highlighted when it's code or JSON). What it links to and what links to it are listed too. Buttons: Edit, Rename, Copy. - A namespace's page: its
READMEdefinition's doc, if it has one, then what's in it, grouped by role (apps, types, data, jobs, server, browser, shared, tests, files), each with the first paragraph of its doc, and its sub-namespaces.
Clicking a definition in the Codebase view opens its page (Open source in its menu opens the text document); a namespace's Open page button (the book) opens the namespace's.
32.4 Finding things
tcm: Find (Cmd/Ctrl+Alt+U, or the search icon on the Codebase view) is one search box for everything: names
(the letters in order, best matches first), words in docs, and types. Anything that looks like a type,
(xs: T[]) => T, or starts with :, is a type search. Results show signatures and doc summaries,
and open as pages.
Search (the first view in the # sidebar, or tcm: Search the codebase's code, Cmd/Ctrl+Alt+Shift+F, which
searches for the selection) is find in files for the codebase: every line of its definitions and pages with the
text in it, grouped by definition, with Match Case, Match Whole Word and Use Regular Expression as in
VS Code's own search. Definitions are searched as they read now, with current names (a rename is in the next
search), and the header line of their documents isn't. Click a definition to open its page, or a line to open the page at
it, the match marked (Alt/Option-click for the definition's document there). The mirror's files and .tcm aren't part of it.
32.5 VS Code
vscode is the extension: definitions of tcm's codebase (tcmts.vscode), packed as a package of its own,
tcm-ts-vscode. Beyond starting tcm lsp in workspaces with a codebase, it adds:
The # icon in the activity bar opens these views:
Apps. A launcher: every app in the codebase, meaning every browser entry point (a function that runs in the browser, takes no arguments and isn't called by other code, like
polls.main, and isn't a React hook, nameduse…). ▶ Start runstcm devfor it on a free port, with its log in an output channel, and opens it in VS Code's built-in browser; the status bar shows running apps. Open (also in an external browser), Log and Stop act on a running one. Clicking an app opens its page; right-click to open its source or edit it. An app only expands to show problems that would stop it building; what it's made of is in the other two views.Codebase. Namespaces, and the definitions and files (pages, configuration, images) in each. Namespaces with more than a few are grouped by what they're for: Apps, Types, Data (tables), Jobs (schedules), Server, Browser, Shared (runs anywhere), Tests and Files. Apps here show whether they're running, with the same start, open and stop buttons. Icons show kinds, endpoints (a cloud) and test results. Hover for details; click to open. Right-click (or the inline icons) for Edit in a scratch file, Find usages, Run (
tcm runin a terminal), Rename (tcm mv, also for a whole namespace) and Copy hash.Changes. What's changed, by history. Branches lists every branch, this one first, each with how many names differ from this one's and when it last changed; another branch opens to those names, and right-click merges it here. History is this branch's entries, newest first: each is the command that made it, and opens to the names it changed (edited, re-hashed because of what it uses, added, removed, renamed). An edited definition opens in VS Code's diff editor, its code before and after, each side with its own names (the entry's before and after it, the other branch's); anything else opens its page. A branch's or an entry's Open changes button shows all the code it changed in VS Code's multi-file diff editor (Show changes as text is
tcm diff), and Reset names to here brings the names back to an entry, as a new entry on top (tcm reset).Data. Every table: declared ones and ones with rows. Each shows its row count and how many rows are still stored as an older row type; Migrate rows brings those up to date (
tcm data migrate), and Go to definition opens thetable<…>()that declares it. Click a table to open it in the table viewer: a grid with a column per field of the row type (each header gives the field's type), the row's id and when it was written.- Click a header to sort by it; type in the search box to find rows containing the text anywhere. Sorting, search and paging (a hundred rows at a time) run in SQLite on the server, so big tables stay fast.
- Click a row to see its full value, nested objects and all, beside the grid.
- Rows stored as an older row type are badged, and the header offers Migrate rows.
- It's live: open viewers refresh as a running app writes rows. It's read-only; changing rows by hand would bypass the table's own type checks and migrations.
Packages. The npm packages the codebase runs (packages): each package with its locked version and how many definitions use it, by range when it runs more than one (React 18 and 19, say), and under each, the definitions using it. Click one to open its package page: its description, license, homepage and npm links, the other ranges the codebase runs, Upgrade (
tcm upgrade <package>: newest versions within the range, nothing re-hashed) and Move to another range (tcm upgrade <package>@<range> [namespace]), then tabs (the one you chose stays chosen as the page refreshes):- README: the package's (the
@typespackage's, when it has none); - API: as its types declare it, every export with its signature (highlighted) and JSDoc, in a foldable section per kind (functions, classes, interfaces, types, enums, values, namespaces), all open to start with;
- Used by: the definitions using it, linking to their pages.
A definition page's Packages list links to these pages too.
- README: the package's (the
Releases. The codebase's own packages (publishing), each with its latest version. Under each:
- Next: what its next release would be (the version, and why: breaking changes, additions, or the same API), and every semver finding behind it, coloured by level: what changed and how, with the lint and what it means for code using the package on hover. Click a finding to open its definition.
- its releases, newest first, with the summary of each; hover for the notes.
Release (the rocket) shows the version and its notes, and asks; afterwards it offers Push, Pack and the package page. Draft release notes opens
release-<package>.mdto write a summary in; above it, a lens says what the release would be as things stand and releases it from there (saving the file first), and another regenerates the list of changes. Pack packs the package, or a release of it, and Save changelog… writes its changelog where you choose. Click a package to open its package page: what it exports and its commands, the next release with every finding and its notes as they'd be, the whole changelog rendered, and the same buttons.
All five refresh when the codebase changes (including from a terminal), and the Data view also when the data does, so rows appear as a running app writes them.
- tcm: Find by type (the search icon in the sidebar, or the command palette): type a TypeScript type, pick a match, and it opens.
- A TypeScript server plugin (
vscode/files/plugin, loaded through VS Code'stypescriptServerPlugins): Go to Definition skips tcm's generated modules, so Cmd+click on a codebase name lands on its source rather than offering.tcm/cache/<hash>.tstoo. Reload the window after installing. - tcm: Add / Update, which save the file first; Rename and Edit ask for names when run from the palette.
Try it on the polls example.
It needs VS Code 1.143 or later (it's an ES module). Pack it from tcm's repository, build a .vsix from what
that writes (the first npx downloads vsce, which takes a while), and install it with Extensions → … →
Install from VSIX…, or with VS Code's CLI (on macOS it's inside the app if code isn't on your PATH):
pnpm tcm pack tcm-ts-vscode --out dist/vscode
cd dist/vscode && npm install && npx @vscode/vsce package
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" --install-extension tcm-ts-*.vsix
The .vsix takes its version from the extension's latest release (tcm release tcm-ts-vscode); unreleased,
it's 0.0.0.
Don't symlink the folder into ~/.vscode/extensions: VS Code only keeps extensions listed in its manifest, and
marks unlisted folders as removed. A workspace you haven't trusted yet runs in Restricted Mode, where the
extension stays off until you trust it.
Set tcm.path if tcm isn't on your PATH: either the path to tcm's cli/bin/tcm.js (spaces in the
path are fine; it's run with Node), or a command line with quotes around words containing spaces. Inlay hints
must be enabled (editor.inlayHints.enabled, on by default) to see watch values.
32.6 Other editors
Any LSP client can run tcm lsp for TypeScript files, next to its TypeScript server. The workspace root must be
the codebase directory (or inside it).
Packages tcm provides (ones the project hasn't installed; see packages) are resolved for the
editor by tcm's TypeScript plugin, vscode/files/plugin. Load it in your TypeScript server (as a
tsserver plugin) to get the same resolution; without it, TypeScript reports those imports as missing, though
tcm's own checks and diagnostics are unaffected.
Neovim (0.10+):
vim.api.nvim_create_autocmd("FileType", {
pattern = "typescript",
callback = function(args)
local root = vim.fs.root(args.buf, { ".tcm" })
if root then vim.lsp.start({ name = "tcm", cmd = { "tcm", "lsp" }, root_dir = root }) end
end,
})
vim.lsp.inlay_hint.enable(true)
vim.lsp.codelens.refresh() -- or map vim.lsp.codelens.run to run the Add/Update actions
Helix (languages.toml):
[language-server.tcm]
command = "tcm"
args = ["lsp"]
[[language]]
name = "typescript"
language-servers = ["typescript-language-server", "tcm"]
Zed needs an extension to register a language server; there isn't one yet.
32.7 Notes
- Scratch files and
tcm:documents get tcm's features. Files elsewhere (an imported project's sources) are ignored. - The server analyzes with the same TypeScript 7 compiler process as the watcher: each open file keeps its own snapshot, so updates take tens of milliseconds.
- Its stdout is the protocol channel. Output from watch expressions and tests goes to its stderr, which editors show in the server's log.