Files
The in-memory filesystem behind the workbench, and the explorer the reader works in
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:
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:
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 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 for the full list. The context menu carries New File, the clipboard block below, Rename and Delete, followed by anything an extension contributed.
#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 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.