/**
* What `ls` finds where shuttle stands, what each row turns out to be, and how
* each is written back.
*
* A listing prints names a person then types, so a row carries the operand
* `cd` takes to reach it and not only the name it goes by. The two are the
* same string for nearly every row and differ wherever a name's own characters
* are readings — a key called `..`, one holding the separator — which is a
* question about the place rather than about the listing, so
* `operandForChild` answers it and this module asks. Registered pieces outside
* the current space, scope, or document root carry their complete reference
* instead; selection preserves it, while `cd` refuses foreign spaces.
*
* A row also carries what it is. The kind is recorded as the row is made
* rather than worked out again by whoever reads it, which is what lets a
* handle minted off a listing say what it stands for without a second read
* (decision 27, `docs/plans/shuttle/README.md`) and what lets a callable be
* annotated where it is listed rather than found by asking a piece for its
* verbs.
*
* The reads are `packages/cli`'s, over the connection this process holds: a
* space root has nothing to read, the two facets are `listSpaceSlugs` and
* `listPieces`, and the cell inside a piece is `getCellValue` for what its
* keys hold and `listCallableKeys` for which of them are verbs. Each takes
* the connection through `deps.loadPieces`, which is the seam a held
* connection fills.
*
* The cell is read for its value where a listing of keys alone would do, and
* that costs nothing: `listCellKeys` (`lib/cell-listing.ts`) is `keysOf` over
* exactly this read, so what a kind costs is the classification and not a
* round trip. `keysOf` is still what names the rows, so a listing lists what
* that seam lists.
*/
import { idStringForEntityAddress, isStreamValue } from "@commonfabric/runner";
import { renderCellReference } from "@commonfabric/runner/shared";
import { isObjectOrArray } from "@commonfabric/utils/types";
import { keysOf } from "../cell-listing.ts";
import {
getCellValue,
listCallableKeys,
listPieces,
listSpaceSlugs,
type PieceConfig,
type SpaceConfig,
} from "../piece.ts";
import type { HeldConnection } from "./connection.ts";
import { quoteToken } from "./line.ts";
import { marker } from "./page.ts";
import {
escapeControlCharacters,
type Facet,
FACETS,
HANDLE_SIGIL,
holdsControlCharacter,
operandForChild,
type PiecePosition,
type Place,
} from "./place.ts";
/**
* What a listing of `slugs/` is a listing of. `listSlugs`
* (`@commonfabric/piece`) is where that bound is stated and where the reason
* for it lives; what this carries is the part a reader of the listing needs,
* which is that the rows are not every name the space answers to.
*/
const SLUG_INDEX_BOUND = "the space's slug index names these, and a slug it " +
"never recorded still resolves";
/**
* What a row turned out to be, recorded where the row is made.
*
* Six kinds and no seventh, closed by the projection {@link ANNOTATED} makes
* of them: what stands at a space root is a facet, which is a `container`;
* what stands in the two facets is a `slug` or a `piece`; what stands inside a
* piece is a `container` where a walk continues through it, a `callable` where
* the position is a verb's dispatch surface, and a `value` otherwise; and a
* `watch` is what `watches` numbers, which is a session object rather than
* anything standing at a place.
*
* A link is not among them, and its absence is a fact about the read rather
* than a gap in the vocabulary: a cell read resolves a link on the way past,
* so what a listing is handed at a position holding one is what it points at.
* Marking such a row is the open question `docs/plans/shuttle/pathref.md`
* leaves for `ls`, and answering it means a read that stops before a crossing
* rather than another arm here.
*/
export type RowKind =
| "container"
| "value"
| "callable"
| "piece"
| "slug"
| "watch";
/** One thing a listing found standing where it was read. */
export interface ListingRow {
/** What the row is called where it stands. */
readonly name: string;
/** What the row turned out to be. */
readonly kind: RowKind;
/**
* The operand `cd` takes to reach it, as `cd` reads it, and absent where
* `operandForChild` offers none. Absent is the narrower claim it makes:
* that no spelling `operandForChild` tries names the row, not that nothing
* reaches it. A registered piece outside the current space, scope, or
* document root supplies its complete reference as the operand instead.
*
* It is the decoded operand rather than the quoted token a line writes it
* as. A row is read back by two consumers that want different forms — a
* line prints it, and a `%n` walks it — and quoting is the printer's step
* ({@link listingLines}). Storing the printed form instead would leave the
* walker undoing it, which is one grammar written twice and the pair free
* to disagree: a key called `first name` prints as `'first name'`, and a
* walk of those characters looks for a key whose name holds the quotes.
*/
readonly operand?: string;
/**
* What the row calls itself, where the read found it a name of its own, and
* absent where it found none.
*
* It is what the fabric holds rather than anything to type: a piece carries
* a name and no address is made of it, so it is written beside the operand
* rather than in place of one. What reaches the row is the operand, whether
* a line prints it or a `%n` walks it.
*/
readonly ownName?: string;
/** What the read said is wrong with what the name points at. */
readonly error?: string;
/**
* The watch a `watch` row stands for, by the key that names one
* (`watch.ts`), and absent on every other kind of row.
*
* It is here because a handle is a bound reference rather than a row number
* (decision 27), and what binds a watch row is neither of the two things
* every other row is bound by: a watch stands at no place, and its name is a
* rendering rather than an identifier. The key is the complete reference of
* the cell it watches, so a row read back names the watch it was minted for
* whatever has been armed or disarmed since.
*/
readonly watch?: string;
}
/** What `ls` found at a place. */
export interface Listing {
/** The rows, in the order the read listed them. */
readonly rows: readonly ListingRow[];
/**
* What the rows are, where they are not everything standing there. A
* listing that said nothing would be read as complete.
*/
readonly bound?: string;
}
/**
* The handles one listing minted: its rows, numbered from `%1` in the order
* they were listed, and the place they were listed at.
*
* The place is what makes a handle a bound reference rather than a row number
* (decision 27): a row's name is a name inside that place, so the pair names a
* cell from anywhere and goes on naming it after the place has moved. For a
* callable row the same pair is the receiver and the verb name `call` wants —
* the place being the receiver and the name the verb — so neither is recorded
* a second time on the row, where the two copies would be free to part.
*/
export interface ListingHandles {
/** Where the listing was read, which its rows stand inside. */
readonly place: Place;
/** The rows in the order they were numbered: `%1` is the first. */
readonly rows: readonly ListingRow[];
}
/**
* What a listing prints as: its rows, one line each, and the bound's line
* where it carries one.
*
* They are two fields rather than one array because a page treats them
* differently. The rows are what a page cuts and what `--limit` counts; the
* bound is shown whatever either says, being the listing's own account of what
* it is a listing of.
*/
export interface ListingRendering {
/** The bound's line, where the listing carries a bound. */
readonly bound?: string;
/** One line per row, numbered, in the order the listing lists them. */
readonly rows: readonly string[];
}
/**
* The reads a listing is made of, each `packages/cli`'s own. A caller supplies
* its own to drive this module with nothing behind it.
*/
export interface ListingDeps {
/** Lists the slugs the space's index records. */
readonly listSpaceSlugs?: typeof listSpaceSlugs;
/** Lists the space's pieces. */
readonly listPieces?: typeof listPieces;
/** Reads the cell a listing inside a piece names its keys off. */
readonly getCellValue?: typeof getCellValue;
/** Names which of that cell's keys stand at a verb's dispatch surface. */
readonly listCallableKeys?: typeof listCallableKeys;
/**
* The line's cancel. A listing inside a piece is two reads, and it is one
* act to the guard the verb runs it under, so the rule that every read has
* a check in front of it with nothing awaited between (`VerbDeps.signal`,
* `vocabulary.ts`) reaches the second read only through this. A listing
* cancelled between its reads comes back empty, which that guard drops on
* the way back.
*/
readonly signal?: AbortSignal;
}
/**
* What `ls` finds at `place`, read over `connection` with `config` saying what
* to connect as.
*
* A row the read reported a failure against is still a row: a name the space
* has and nothing resolves is a name the space has, so it comes back carrying
* what went wrong rather than being dropped, and it never takes the listing
* down with it. A read that failed outright is no listing, and raises.
*
* @throws Whatever the read throws — an unreachable server, an identity that
* will not load, a path the piece refuses.
*/
export async function listPlace(
config: SpaceConfig,
place: Place,
connection: HeldConnection,
deps: ListingDeps = {},
): Promise
{
const position = place.position;
switch (position.kind) {
case "root":
return {
rows: FACETS.map((facet) => rowFor(place, facet, "container")),
};
case "facet":
return await listFacet(config, place, position.facet, connection, deps);
case "piece":
return await listKeys(config, place, position, connection, deps);
}
}
/**
* What `ls` prints for `listing`: one line per row, and the bound's own line
* beside them.
*
* The lines come back as lines rather than as one string, because how many of
* them fit is `page.ts`'s question and not this module's. What this decides is
* what each line says, and which of them are rows.
*
* A row opens with the handle a person types to reach it again, in a column
* the widest of them sets, and its name comes next in a column of its own. The
* name is written as a token, so what a reader copies out of the name column
* is what `cd` takes; a name a row has none for stands as a marker in that
* column instead — and `quoteToken` is what keeps the two apart, not anything
* here: `<` is one of the characters the grammar reserves, so a name holding
* one is printed quoted and a name never opens with it. The name column
* therefore opens with `<` exactly where the row has no name.
*
* Everything else on a line — the row with no operand, the name a row calls
* itself by, a callable's annotation, a row's error, and the bound — is
* written as a marker. Those brackets delimit for a reader and not for a
* parser: a payload may hold an angle bracket of its own and nothing escapes
* it. Nothing parses a listed line, and a form that could be parsed is a
* second output form rather than a rule for this one
* (`docs/plans/shuttle/futures.md`).
*
* The bound comes back beside the rows rather than among them, because it is
* not one of them: a page cuts rows and always shows the bound, and `--limit`
* counts rows and never counts the bound. What it says — that these rows are
* not everything standing here — is what a reader of a partial listing most
* needs, so it is the one line a page may not drop.
*
* A row is one line, and lines are separated by a newline, so nothing written
* on a line may put one inside it: a message and a bound each have their
* newlines written as spaces, and a name carrying one is described rather than
* written. What a message does with the rest of the class a terminal acts on,
* and why a name cannot do the same, is with {@link oneLine}. Rewriting one
* inside a name would leave a token `cd` no longer takes back to the row,
* which is the guarantee the name is printed for, so what a terminal makes of
* it is not something this can spend that on.
*/
export function listingLines(listing: Listing): ListingRendering {
const rows = numbered(listing.rows.map(lineFor));
return listing.bound === undefined
? { rows }
: { bound: marker(oneLine(listing.bound)), rows };
}
/**
* `lines` with the handle each one is named back by in front of it, right
* aligned so that every line's own text opens in the same column.
*
* Alignment is over the whole listing rather than over a page of it, so a row
* sits in the same column on the page it first appeared on and on the page
* `more` writes: the widest number the listing will hand out is known once the
* rows are, and it is what every page is padded to.
*
* Every listing shuttle writes goes through here, so a verb that composes its
* own lines numbers them the way `ls` numbers its rows without restating how.
* The numbering is positional in the array, which is also how a handle table
* reads a row back (`handles.ts`), so the two agree by construction rather
* than by carrying a number apiece.
*/
export function numbered(lines: readonly string[]): readonly string[] {
const column = handleFor(lines.length).length;
return lines.map((line, index) =>
`${handleFor(index + 1).padStart(column)} ${line}`
);
}
/**
* The handle a listing's `number`th row is reached by, which is what a person
* types to name it again.
*
* Numbering runs from one rather than from zero: these are read off a screen
* and typed back, which is what a shell's job numbers and a printed list's
* items do, and neither starts at zero.
*/
export function handleFor(number: number): string {
return `${HANDLE_SIGIL}${number}`;
}
/**
* Helper for {@link listPlace}, which is what `facet` lists at `place`.
*
* The two facets read different things and are written out one arm each rather
* than chosen from a table, so that a facet added to {@link FACETS} fails to
* compile here instead of silently taking the other one's read.
*
* A piece stands as its id and carries the name it holds beside it, where
* `listPieces` read one. An id alone is the operand only when its complete
* reference names the local document at the current scope. Otherwise the
* registered reference is the operand, preserving a foreign space, scope,
* or path instead of resolving that id against the listing's space.
*/
async function listFacet(
config: SpaceConfig,
place: Place,
facet: Facet,
connection: HeldConnection,
deps: ListingDeps,
): Promise {
const loadPieces = () => connection.pieces();
switch (facet) {
case "slugs": {
const slugs = await (deps.listSpaceSlugs ?? listSpaceSlugs)(config, {
loadPieces,
});
return {
rows: slugs.map((row) =>
rowFor(place, row.slug, "slug", { error: row.error })
),
bound: SLUG_INDEX_BOUND,
};
}
case "pieces": {
const pieces = await (deps.listPieces ?? listPieces)(config, {
loadPieces,
});
return {
rows: pieces.map((row) => {
const entry = rowFor(place, row.id, "piece", {
ownName: row.name,
error: row.error,
});
const localReference = renderCellReference({
space: place.position.space,
id: idStringForEntityAddress(row.id),
scope: place.scope,
path: [],
});
return row.reference === localReference
? entry
: { ...entry, operand: row.reference };
}),
};
}
}
}
/**
* Helper for {@link listPlace}, which is what the cell `position` names lists
* at `place`.
*
* The piece and the scope ride the config and the path is the read's operand,
* which is where `getCellValue` takes one: the config's own `piecePath` is
* dropped by the resolution, so a path written in both places would be walked
* once whichever way it arrived. A place stands on the piece as an operand
* named it, a slug included, so what makes a slug typed back off a listing
* reach its piece is the read's own resolution step
* (`PieceResolutionDeps.resolvePieceAddress`).
*
* An empty listing means the path names a leaf, and nothing here says which:
* `keysOf` gives a leaf no keys and gives an empty container none either, and
* telling the two apart is not something this read can do.
*
* Which keys are callables is a second read, of the cell rather than of its
* value: a stream's position holds nothing, and what says it is a stream is
* the schema its stored links carry, which is what `listCallableKeys` reads
* for each key the first read named.
*/
async function listKeys(
config: SpaceConfig,
place: Place,
position: PiecePosition,
connection: HeldConnection,
deps: ListingDeps,
): Promise {
const pieceConfig: PieceConfig = {
...config,
piece: position.piece,
pieceScope: place.scope,
};
const pieceDeps = { loadPieces: () => connection.pieces() };
const level = await (deps.getCellValue ?? getCellValue)(
pieceConfig,
[...position.path],
{},
pieceDeps,
);
const keys = keysOf(level);
// The check in front of the second read: the first was awaited, and a
// cancel that landed meanwhile is one this read must not go out after.
if (deps.signal?.aborted) return { rows: [] };
const callables = keys.length === 0
? new Set()
: await (deps.listCallableKeys ?? listCallableKeys)(
pieceConfig,
[...position.path],
keys,
{},
pieceDeps,
);
return {
rows: keys.map((key) =>
rowFor(
place,
key,
kindOf(childOf(level, key), callables.has(key)),
)
),
};
}
/**
* Helper for {@link listKeys}, which is what the cell holds at `key`, and
* nothing where it holds nothing there.
*
* `keysOf` names only the keys of an array or an object, so the cast is over a
* value already known to be one of those; a key holding `undefined` is a key
* all the same, and what it stands for is a value rather than a container.
*/
function childOf(level: unknown, key: string): unknown {
return (level as Record)[key];
}
/**
* Helper for {@link listKeys}, which is what a position holding `value` is,
* given whether the piece's stored links say it is a `callable`.
*
* A stream is a dispatch surface rather than a value — nothing is stored at
* it to read, and a read aimed at one is refused (`classifyReadPathVerb`,
* `lib/piece.ts`) — so it is the piece's callable and is marked as one. That
* is decided off the same link-derived schema the refusing read decides off
* (`listCallableKeys`), so a listing and the read that refuses such a
* position agree about which positions those are. A position that still
* reads as the retired `{ $stream: true }` sentinel is a callable too, until
* no stored document holds one.
*
* Everything else divides by whether a walk continues through it: an array or
* an object holds keys and `cd` descends into it, and anything else is where a
* path ends.
*/
function kindOf(value: unknown, callable: boolean): RowKind {
if (callable || isStreamValue(value)) return "callable";
return isObjectOrArray(value) ? "container" : "value";
}
/**
* Helper for {@link listPlace}, which is the row `name` stands as at `place`,
* being a `kind` and carrying whatever else the read `found` against it: the
* name the row calls itself by, and what went wrong where something did.
*
* The two arrive in one argument because they are one thing to a caller — the
* part of a row a read supplies rather than the place — and because two bare
* strings at the end of a call are two a caller can hand over the wrong way
* round with nothing to say so.
*/
function rowFor(
place: Place,
name: string,
kind: RowKind,
found: { readonly ownName?: string; readonly error?: string } = {},
): ListingRow {
const operand = operandForChild(place, name);
return {
name,
kind,
...(operand === undefined ? {} : { operand }),
...(found.ownName === undefined ? {} : { ownName: found.ownName }),
...(found.error === undefined ? {} : { error: found.error }),
};
}
/**
* Which kinds a listed line annotates, and with what.
*
* A projection over every kind rather than a test for the one that is marked,
* closed by the compiler: a kind added to {@link RowKind} without a line here
* does not compile, so whether it is annotated is a decision somebody made
* rather than an absence nobody noticed.
*
* Only the callable is marked, and what marks it is what `grammar.md`
* promises: a piece's callables surface inline in listings, annotated as
* callable, rather than behind a reserved name. The rest are not marked
* because the annotation would be on every row and would say what the next
* `ls` says anyway — a container lists, and a value does not. A watch is that
* case at its plainest: every row of the one listing that mints one is a
* watch, so the mark would be on all of them and distinguish none.
*/
const ANNOTATED = {
container: undefined,
value: undefined,
callable: "callable",
piece: undefined,
slug: undefined,
watch: undefined,
} satisfies Record;
/**
* Helper for {@link listingLines}, which is the line `row` prints as, without
* the number {@link numbered} puts in front of it.
*
* A row's own name is written as a marker, which is what keeps it off the
* column a reader types back: a name the fabric holds is not an address, and
* one printed where the operand goes would offer a spelling no verb takes. It
* goes through {@link oneLine} for the reason an error's text does — it is a
* value a read served rather than a name this module made — so a name holding
* a break stays one row, and one holding a character a terminal acts on prints
* as the glyph naming it.
*/
function lineFor(row: ListingRow): string {
const annotation: string | undefined = ANNOTATED[row.kind];
return [
row.operand === undefined
? marker(noOperandFor(row.name))
: quoteToken(row.operand),
...(row.ownName === undefined ? [] : [marker(oneLine(row.ownName))]),
...(annotation === undefined ? [] : [marker(annotation)]),
...(row.error === undefined
? []
: [marker(`error: ${oneLine(row.error)}`)]),
].join(" ");
}
/**
* Helper for {@link listingLines}, which says that a row called `name` has no
* operand, showing the name where a line has room for it.
*
* It says no operand rather than that nothing reaches the row, which is the
* wider claim and a false one: a key whose first character is `#` is reached
* by a multi-segment operand, `#` being data in a segment that names a data
* key.
* What holds of every row this is written for is that neither the name nor the
* reference names it, and a name is what a listing prints.
*
* Two names are described rather than written, and for the same reason the
* doors refuse them: one holding a line break would make a second row out of
* one wherever the row is written, and one holding a control character is an
* instruction on a terminal the row is read on. Writing either would put
* back, in the marker, exactly what
* refusing the name kept off the screen. The name is shown
* as it is written and not as something to type — it sits inside a marker,
* which by the rule above carries no name.
*
* A name holding a newline is described rather than shown, because a row is
* one line. That row is also one nothing reaches at all, a segment holding a
* break being refused at every door, so what the description withholds is a
* string no operand would have carried anyway.
*/
function noOperandFor(name: string): string {
return name.includes("\n")
? "no operand: a name holding a line break"
: holdsControlCharacter(name)
? "no operand: a name holding a control character"
: `no operand: ${quoteToken(name)}`;
}
/**
* Helper for {@link listingLines}, which is `text` fit to be written on a
* line and acted on by nothing: each newline as a space, so that what is
* written on a line takes one line, and each remaining character a terminal
* acts on as the glyph that names it.
*
* The payloads that are a caller's own text go through it, the bound as much
* as an error — the two agreeing is what keeps "a row is one line" a rule
* rather than a habit of one of them. The marker for a row with no operand
* builds its own text and answers a break, or a character a terminal acts on,
* by describing the name instead of showing it.
*
* The two rewrites are two decisions. A newline becomes a space so that a
* message stays one row; the rest become glyphs so that a message a person
* reads cannot instruct the terminal it is read on. Escaping is what a message
* gets and a name does not, because a message is read where a name is typed
* back.
*
* Nothing else is touched, an angle bracket included: those delimit a marker
* for a reader rather than for a parser, and a payload holding one is that
* decision rather than this one.
*
* A verb composing rows of its own writes the prose on them through here, so
* one rule holds for every annotation shuttle prints beside a row rather than
* one per producer.
*/
export function oneLine(text: string): string {
return escapeControlCharacters(text.replaceAll("\n", " "));
}