
# Files

The workbench shows a tree of files, and reads and writes it directly — no request in between.
This page covers `FileSystem`, the object that tree is, and the explorer: creating, renaming,
moving and dropping files into it.

## Creating one

`FileSystem`, imported from `codelet/workbench`, is an in-memory tree. Create one empty or seed
it with a map of path to text:

```ts
import { FileSystem } from "codelet/workbench";

const fs = new FileSystem({
  "/package.json": '{ "name": "demo" }',
  "/src/index.ts": "export const hi = () => 'hi';",
});

fs.write("/src/util.ts", "export const noop = () => {};");
fs.read("/src/index.ts"); // "export const hi = () => 'hi';"
fs.exists("/src/util.ts"); // true
fs.stat("/src"); // "directory"

const unsubscribe = fs.onDidChange((changes) => {
  for (const { type, path } of changes) console.log(type, path);
});
```

Pass it to a `Workbench` as `fs`, or to `Editor` for a single file — `FileSystem` is what the
tree, the tabs, the explorer and every extension read and write in common.

| Member                     | Description                                                                         |
| -------------------------- | ----------------------------------------------------------------------------------- |
| `new FileSystem(seed?)`    | Empty, or seeded from a map of path to text.                                        |
| `fs.version`               | A counter bumped on every mutation — cheap to key a re-render off.                  |
| `fs.read(path)`            | The file's text. Throws `ENOENT` if it doesn't exist, `EISDIR` for a directory.     |
| `fs.write(path, content)`  | Creates missing parent directories. A no-op if the content is unchanged.            |
| `fs.exists(path)`          | Whether anything is at that path.                                                   |
| `fs.stat(path)`            | `"file"`, `"directory"`, or `undefined`.                                            |
| `fs.createDirectory(path)` | Throws `EEXIST` if something is already there.                                      |
| `fs.delete(path)`          | Recursive. Throws `EPERM` on the root.                                              |
| `fs.rename(from, to)`      | Throws `EEXIST` if `to` exists, `EPERM` moving a directory inside itself.           |
| `fs.readDirectory(path)`   | One level of `FileEntry`, sorted directories-first, then by name.                   |
| `fs.tree()`                | The whole tree as one `FileEntry`, rebuilt once per mutation.                       |
| `fs.files()`               | Every file path, in tree order.                                                     |
| `fs.onDidChange(listener)` | Called with the batch of changes after a mutation. Returns an unsubscribe function. |

## Errors

A mutation that can't happen throws a `FileError`, with a `code` of type `FileErrorCode`:

| Code      | Meaning                          |
| --------- | -------------------------------- |
| `ENOENT`  | no such file or directory        |
| `EEXIST`  | file already exists              |
| `EISDIR`  | illegal operation on a directory |
| `ENOTDIR` | not a directory                  |
| `EPERM`   | operation not permitted          |

`FileError` also carries `syscall` (what was attempted, e.g. `"rename"`) and `path`.

## Types

A `FileEntry` is `{ name, path, type: "file" | "directory", children? }` — `children` is present
only on directories. A `FileChange` is `{ type: "created" | "changed" | "deleted", path, before? }`
— `before` is the text a `changed` file held a moment ago, present only for that type. A
`FileListener`, the shape `onDidChange` takes, receives an array of these: a rename, for
instance, reports a delete and a create together.

## Path helpers

`codelet/workbench` also exports the path arithmetic `FileSystem` is built on:

```ts
import { basename, dirname, isInside, join, normalize } from "codelet/workbench";

basename("/src/index.ts"); // "index.ts"
dirname("/src/index.ts"); // "/src"
join("/src", "index.ts"); // "/src/index.ts"
isInside("/src", "/src/index.ts"); // true
normalize("src/../lib/x.ts"); // "/lib/x.ts"
```

Every path is `/`-rooted with no trailing slash, so `"src/x.ts"` and `"/src/x.ts"` name the same
file once normalized. `normalize` resolves `.` and `..` segments; `dirname("/")` and
`basename("/")` answer `"/"` and `""` — the root holds itself and has no name of its own.

::note
`FileSystem` is synchronous by design: the explorer, the command palette and every language
server read it directly, in the middle of a render. [Remote](/extensions/remote) mirrors
a real filesystem into one of these rather than replacing it.
::

## The explorer

Click a directory row to expand or collapse it; click a file to open it. The funnel icon in the
heading opens a filter field — narrow it's shown outright, wide it's VSCode's type-to-filter:
start typing while the tree has focus and the field opens with that character already in it.
Filtering narrows the tree to files whose names match, with the directories on the way to them
left in view; Escape clears it.

The plus icon, or **New File** in the context menu, names a new file into a directory — type a
path with a slash (`lib/x.ts`) to create it several directories deep at once. **Rename**, or
`Enter` on the focused row, edits a name in place, with everything but the extension
pre-selected. **Delete** asks for confirmation first, warning that a folder's contents go with
it. Dragging a row onto a directory moves it there; dropping on the empty area below the last
row moves it to the root.

Arrow keys move the focused row (right expands a folder or steps into it, left collapses or
steps out); the tree is one tab stop, not one per row. See
[Keyboard shortcuts](/guide/keyboard) for the full list. The context menu carries New File, the
clipboard block below, Rename and Delete, followed by anything an extension contributed.

::codelet-playground{mode="workbench" active="src/index.ts" height="440"}

```ts [src/index.ts]
import { greet } from "./lib/greet.ts";

console.log(greet("codelet"));
```

```ts [src/lib/greet.ts]
export const greet = (name: string) => `hi ${name}`;
```

```md [README.md]
# Demo

Try renaming a file, dragging one into another folder, or right-clicking for the context menu.
```

```json [package.json]
{
  "name": "codelet-demo",
  "private": true
}
```

::

## Cut, copy and paste

`Ctrl`/`Cmd`+`X`, `C` and `V` on the focused row — or the same three in the context menu — cut,
copy and paste. A paste lands in the directory the row it's pasted on stands for (a file's own
directory, for a file row; the root, for the pane below the tree). Pasting a copy back where it
came from duplicates it as `index copy.ts`; cutting is the move a drag is, and is refused where
the name is already taken. **Copy Path** and **Copy Relative Path** write the path alone to the
system clipboard as text.

That's also where cut and copy write: a directory is nothing a browser clipboard can hold, and a
file here is text the workspace holds rather than bytes the system has a file for, so the path
is the most either gesture can leave outside the page. The useful direction is the other one — a
file copied in a file manager, another tab, or a screenshot pastes straight into the tree, asking
the same replace-or-skip question a drop would.

::note
On an insecure origin, where the Clipboard API is missing rather than refusing, Copy Path and
Copy Relative Path are left out of the menu.
::

## Dropping files in

Drop a file from the desktop onto a row and it lands in that row's directory (a file's own, for
a file row); onto the pane below the tree and it lands at the root; onto the editor and it lands
beside the file that pane is showing. A dropped folder brings everything under it. Nothing has
to be enabled for this — a name the workspace already holds is asked about once for the whole
drop: Replace, Skip, or Cancel.

## Binary files

A `FileSystem` holds text, so a file that isn't UTF-8 — or that decodes but contains a NUL byte
— is written as a `data:` URI instead. That's the same address form
[`codelet/extensions/media`](/extensions/media) reads a `.png` as, so an image dropped into a
workbench carrying that extension opens and shows normally.

A file held that way and claimed by nothing — a `.pdf`, an archive, an image with no media
extension mounted — opens as itself rather than the address it's held as: a pane naming the
file, its type and its size, with **Download** and **Open Anyway** buttons. **View: Reopen
Editor With…** is the way to the raw text underneath, from the command palette.

::warning
Base64 runs about a third longer than the bytes it encodes, and stays in memory for the life of
the page, so a dropped file over 16 MiB is refused rather than taken. Anything else a drop
couldn't add is reported once the drop finishes — the rest of the batch still lands.
::

## Workspaces

An extension can open a tree of its own over the reader's, showing in the explorer and the
tabs in its place — `codelet/extensions/github` opening a repository is one example. Closing it
puts the reader straight back: their own tabs, cursors and unsaved edits are exactly where they
left them, because nothing about the reader's tree was ever touched.

:read-more{to="/guide/workbench"}
