VS Code and editors — tcm-ts

VS Code and editors

tcm's VS Code extension shows, as you type in a scratch file, what each definition is to the codebase, with Add and Update buttons, watch values and test results inline. It also shows the codebase itself: definitions as read-only documents, pages, search, and sidebars for apps, history, data, packages and releases.

Underneath is tcm lsp, a language server that runs alongside TypeScript's own: TypeScript keeps doing completions, type errors, go-to-definition and refactors, and tcm adds what only it knows. Any editor that speaks LSP can run it.

VS Code

Needs VS Code 1.143 or later. The extension (tcm-ts-vscode) is packed 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 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

Reload the window after installing. 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 in its manifest, and marks unlisted folders as removed.
  • In an untrusted workspace (Restricted Mode), the extension stays off until you trust it.
  • Set tcm.path if tcm isn't on your PATH: the path to tcm's cli/bin/tcm.js (run with Node; spaces are fine), or a command line with quotes around words containing spaces.
  • Watch values need inlay hints on (editor.inlayHints.enabled, the default).

It starts tcm lsp in any workspace with a codebase. Try it on the polls example.

Other editors

Run tcm lsp (LSP over stdio) for TypeScript files, next to your 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 by tcm's TypeScript plugin, vscode/files/plugin. Load it in your TypeScript server as a tsserver plugin; without it, TypeScript reports those imports as missing (tcm's own checks 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.

In scratch files

Scratch files are the .ts and .tsx files at the top of the codebase directory, the ones the watcher checks. Unsaved buffers are analyzed as you type.

  • What each definition is to the codebase, as a code lens above it:
    • + new lib.square with Add to codebase;
    • ~ update lib.square (was #a1b2c3d4) with Update codebase;
    • lib.square #a1b2c3d4 · runs on the server once it's saved (placement shown for app code);
    • aliases and conflicts;
    • tcm: 2 type errors; nothing hashed until they're fixed while TypeScript reports errors.
  • Watch values inline, at the end of the line: square(12) ⧩ 144. They're cached by hash, so known values show at once as you type; 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 tests: ✓ passed (cached) or ✗ failed: …, run on open and save.
  • Hovers on codebase names: every path a definition has, its hash, signature, where it runs and why, how many named definitions use it, and its documentation.
  • Quick fixes (Cmd/Ctrl+.): a codebase name the file doesn't import gets Import x from "#codebase/…" (one per namespace that has it); a name it imports from the codebase gets Edit x here too, adding that definition to the file.
  • Debug lenses on tests and runnable definitions (debugging).
  • Formatting on save with oxfmt, and Format Document (formatting).
  • Diagnostics from oxlint (source oxlint, linked to the rule's page; linting) and from tcm (source tcm: a top-level var or declare, say).

Files elsewhere (an imported project's sources) are ignored.

The codebase as documents

  • Definitions are documents. .tcm/src/lib/square.ts is lib.square with current names (what tcm view prints), its first line giving its names, hash and where it runs (.tsx for JSX). They're real files, so debuggers stop in them and TypeScript reads them. They're the codebase's own branch's (the one tcm switch checks out); a session's are in .tcm/sessions/<branch>/src, opened by a language server run with its TCM_BRANCH.
  • They're read-only. Edit writes the definition into the scratch file (scratch.ts, or scratch.tsx for JSX), added to what's there if it holds work, where the lens offers Update codebase. Imports are folded away (the extension sets editor.foldingImportsByDefault).
  • Go to Definition (Cmd/Ctrl+click) on a codebase name opens its document; in a document, every reference is a link. In VS Code, the extension's TypeScript plugin hides TypeScript's own answer (the generated module in .tcm/cache); other editors show both.
  • Find All References lists every definition that uses one, at the exact spot (alongside TypeScript's results in scratch files).
  • Lenses on a document: how many named definitions use it (click to list them), Edit, Rename, Debug.
  • Symbols: workspace symbol search (Cmd/Ctrl+T) covers every name; the outline shows a document's definitions.
  • Hovers work as in scratch files.

Clients can also open a definition as a tcm:/lib/square.ts document, with the same text.

Pages

In VS Code, definitions and namespaces have pages, as in Unison's codebase UI. Each opens in its own tab (opening one already open focuses it), links open more, and open pages refresh as 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 values), and its source with every reference a link (imports too: a name to its definition, #codebase/… to the namespace, a package to its page). Then what it uses, what it's used by, and the packages it imports. Buttons: Source (the tcm: document), Edit, Usages, Run (Start for an app, or open it if it's running), Debug, Rename, Copy (the hash).
  • A file's page: its format and content: Markdown rendered (its {@link}s and links open their pages), an image shown, other text as it is (highlighted for code or JSON), with what it links to and what links to it. Buttons: Edit, Rename, Copy.
  • A namespace's page: its README definition's doc, if any, then its contents grouped by role (apps, types, data, jobs, server, browser, shared, tests, files), each with the first paragraph of its doc, and its sub-namespaces.

Click a definition in the Codebase view to open its page (Open source in its menu opens the document); a namespace's Open page button (the book) opens its page.

Finding things

  • tcm: Find (Cmd/Ctrl+Alt+U, or the search icon on the Codebase view): one box for names (letters in order, best 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.
  • tcm: Find by type (the sidebar's search icon, or the palette): type a TypeScript type, pick a match.
  • Search (the first view in the # sidebar, or tcm: Search the codebase's code, Cmd/Ctrl+Alt+Shift+F, which searches for the selection): find in files for the codebase. Every line of definitions and pages with the text, grouped by definition, with Match Case, Match Whole Word and Use Regular Expression. Definitions are searched as they read now, with current names; their header line isn't searched, nor are the mirror's files and .tcm. Click a definition to open its page, or a line to open the page there (Alt/Option-click for the document).

The sidebar

The # icon in VS Code's activity bar opens these views. They refresh when the codebase changes (from a terminal too), and the Data view also when the data does.

  • Apps. Every app: a browser entry point, a function that runs in the browser, takes no arguments, returns a page (an element, or nothing, as a main does), isn't called by other code and isn't a hook (use…), like polls.main.

    • ▶ Start runs tcm dev on a free port, its log in an output channel, and opens it in VS Code's browser; the status bar shows running apps. Open (or in an external browser), Log and Stop act on a running one.
    • Debug runs it under the debugger, server and browser at once (debugging).
    • Click an app for its page; right-click to open or edit its source. An app expands only to show problems that would stop it building.
  • Codebase. Namespaces, and the definitions and files (pages, configuration, images) in each. Large namespaces are grouped: Apps, Types, Data (tables), Jobs (schedules), Server, Browser, Shared (runs anywhere), Tests, Files. Apps show whether they're running, with start, open and stop. 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 run in a terminal), Debug, Rename (tcm mv, a whole namespace too) and Copy hash.

  • Changes. History and branches.

    • Branches: every branch, this one first, each with how many names differ and when it last changed. Another branch opens to those names; right-click merges it here.
    • History: this branch's entries, newest first, each the command that made it, opening to the names it changed (edited, re-hashed, added, removed, renamed). An edited definition opens in the diff editor, before and after, each side with its own names; anything else opens its page.
    • Open changes (on a branch or entry) shows all its changed code in the multi-file diff editor (Show changes as text is tcm diff). Reset names to here brings the names back to an entry, as a new entry (tcm reset).
  • Data. Every table, declared or with rows, with its row count and rows stored as an older row type. Migrate rows brings those up to date (tcm data migrate); Go to definition opens the table<…>() declaring it. Click a table for the table viewer: a column per field (each header gives its type), the row's id and when it was written.

    • Click a header to sort; type to search anywhere in a row. Sorting, search and paging (100 rows at a time) run in SQLite, so big tables stay fast.
    • Click a row to see its full value beside the grid.
    • Older-row-type rows are badged, and the header offers Migrate rows.
    • It refreshes as a running app writes rows. It's read-only: editing rows by hand would bypass the table's type checks and migrations.
  • Packages. The npm packages the codebase runs (packages): each with its locked version and how many definitions use it, by range when it runs more than one (React 18 and 19, say), and the definitions using it. Click one for its package page: description, license, homepage and npm links, other ranges, Upgrade (newest versions within the range, nothing re-hashed: tcm upgrade <package>) and Move to another range (tcm upgrade <package>@<range> [namespace]), then tabs:

    • README (the @types package's, when it has none);
    • API, as its types declare it: every export's signature and JSDoc, in a section per kind;
    • Used by, linking to their pages.

    A definition page's Packages list links here too.

  • Releases. The codebase's own packages (publishing), each with its latest version. Under each:

    • Next: its next release (the version, and why: breaking changes, additions, or the same API), with every semver finding behind it, coloured by level (hover for the lint and what it means; click to open the definition);
    • its releases, newest first, with summaries (hover for the notes).

    Release (the rocket) shows the version and notes, and asks; then offers Push, Pack and the package page. Draft release notes opens release-<package>.md: a lens above says what the release would be and releases it (saving the file first), another regenerates the list of changes. Pack packs the package or a release of it; Save changelog… writes its changelog. Click a package for its page: exports and commands, the next release with findings and notes, the whole changelog, and the same buttons.

From the palette, tcm: Add / Update save the file first; Rename and Edit ask for names.

Language server reference

Commands

Editors expose these as code-lens actions or palette commands (workspace/executeCommand). Each reports its output as a message.

CommandArguments
tcm.addfile URItcm add the file; it must be saved (tcm reads what's on disk)
tcm.updatefile URItcm update from the file; it must be saved
tcm.mvfrom, totcm mv
tcm.editnamestcm edit them into the scratch file (.tsx for JSX); returns its URI
tcm.editIntofile URI, nameadd a definition to that scratch file
tcm.dataMigratetabletcm data migrate <table>
tcm.upgradepackage, [range, namespace]tcm upgrade within its ranges, or to another range
tcm.releasepackage, [version]tcm release
tcm.releaseDraftpackagewrite its release notes draft; returns its URI
tcm.packpackage, [version]tcm pack

After tcm.add, tcm.update, tcm.mv, tcm.upgrade and tcm.release, every open file's status is refreshed.

Requests

For clients that show more:

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, older-row-type rows, the declaring definition and the row type's fields
tcm/rows
a page of a table's rows: searched, sorted by a field and paged in SQLite; the fields, 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, or tcm:/.data/<table>/<id>.txt
tcm/findByType
type search, as tcm find ':…'
tcm/find
the finder: names, doc words and types
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/branches, tcm/history, tcm/historyEntry, tcm/branchChanges
the Changes view: branches (how many names differ), this branch's 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, by definition: text as written, or /…/ (finding code); caseSensitive, wholeWord, regex (all the text a regular expression), under
tcm/debug
a launch configuration for VS Code's JavaScript debugger: kind run, test or app, with a name or file uri
tcm/packages, tcm/packageUsers, tcm/packagePage
the npm packages the codebase runs, what uses a package at a range, and its page
tcm/publishing, tcm/releaseCheck, tcm/releasePage, tcm/releaseList, tcm/changelog
the codebase's own packages, a package's next release and its findings, its page, its releases, and its changelog as Markdown

Notifications

The server sends tcm/changed when the codebase changes and tcm/dataChanged when the data store does, whether from the editor, a terminal or a running app.

Notes

  • The server uses the same TypeScript 7 compiler process as the watcher, with a snapshot per open file, so updates take tens of milliseconds.
  • Its stdout is the protocol channel. Output from watch expressions and tests goes to stderr, which editors show in the server's log.

Built from c482a14e