Finding code — tcm-ts

17. Finding code

17.1 By name, signature or doc

tcm find <query> (the agents' tcm_find) lists the names with every word of query in the name, its signature text or its doc, in any case: tcm find square number. Those it's in the name or signature of come first, then those it takes the doc to find. Docs, and the code tcm search reads, are as the branch it runs on shows them: a session's changes to a page, a doc or a comment are found on its branch as soon as they're saved, and not on main until they land. Searching another branch (--branch) reads its text as it has it, rendered, never from main's files (or any other branch's).

A signature names the codebase's types as they're named now: after tcm mv lib.User lib.Person, find, view, ls, the editor and the agent tools show (u: Person) => string. (A signature is the type as TypeScript printed it when the definition was made, kept beside it, not part of its hash; the types it names are found by hash, from its shape, and shown with their current names.)

17.2 Text in the code

tcm search <text> (the agents' tcm_search, the editor's Search view) is find in files for the codebase: each line of its definitions (as they read now, with current names) and pages with the text in it, by definition, numbered as tcm view --lines counts them. tcm view <name> --grep <text> (tcm_view's grep) shows a definition's lines with it, and two either side. Both take what to find the same way:

  • Text, as written: a|b is those three characters, ( a parenthesis.
  • A regular expression is written /…/, flags after it: /get|set/i. Its case is its flags': /get/ matches only get.
  • Text is matched in any case by tcm search (--case for its case) and in its case by --grep. --word keeps to whole words. --regex is the old way, all the text a regular expression without slashes (and the editor's Use Regular Expression).

Finding nothing says which it looked for: no lines with the text "a|b" (text, as written: a regular expression is written /…/), or no lines matching /a|b/.

--under <namespace> searches only a namespace, or one definition. A search stops at 500 definitions or 5000 matches, and then says so: it stopped at 500 definitions or 5000 matches: the rest of the codebase wasn't searched; narrow it with more of the text, --word, or --under. Of what it finds, the first 200 lines are shown (--limit <n> for more).

17.3 By type

tcm find ':<type>' finds terms by the shape of their type, as Unison's type search does. The query is a TypeScript type:

$ tcm find ':(xs: T[]) => T | undefined'
type variables: T
lib.first  #q5xh6tqo  <A>(xs: A[]) => A | undefined
lib.head   #glnvgmf6  <T>(xs: T[]) => T | undefined

$ tcm find ':(n: number, s: string) => string'
lib.repeat  #66hwcb26  (s: string, n: number) => string  arguments reordered

$ tcm find ':(id: string) => User'
lib.loadUser  #sjksocbz  (id: string) => Promise<User>  async
  • Code that came from elsewhere (pushed or pulled) comes without the shapes this matches, and gets them as the branch comes to name it; a search works out any still missing.
  • Type variables. Names in the query that are neither codebase types nor TypeScript globals become type variables, so (xs: a[]) => a works like Unison's [a] -> a. Explicit generics (<T>(xs: T[]) => T) work too.
  • Codebase types are found by name: User, or lib.User when the short name is ambiguous (tcm says which paths it could be).
  • Parameter names don't matter, nor do type-variable names: <A>(xs: A[]) => A and <T>(ys: T[]) => T are the same shape.

17.3.1 Ranking

Exact matches (the same type up to renaming) come first. Then, closest first, each labelled:

arguments reordered
the same parameters in another order
async
returns a Promise of the queried result type
more specific
an instance of the query: sum: (xs: number[]) => number for (xs: a[]) => a
more general
the query is an instance of it: last: <T>(xs: T[]) => T for (xs: number[]) => number
partly compared
parts too deep, or conditional or mapped types, were compared only as "some type"

Overloads are searched one by one; a function matches through its closest overload.

17.3.2 How it works

When a term is added, tcm records its type as a shape (metadata, not part of the hash): functions, parameters, arrays, tuples, unions, object types and literals. Type parameters become numbered variables. Codebase types are referenced by hash, so renaming a type (tcm mv lib.User lib.Person) doesn't lose any matches; library types (Promise, Map, a package's types) by name.

The query is typechecked against the #codebase shim, turned into the same kind of shape, and unified with each term's: query variables can stand for parts of a definition's type (more specific) and the definition's variables for parts of the query (more general).

built from c482a14e