Codelet logoCodelet

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.

MemberDescription
new FileSystem(seed?)Empty, or seeded from a map of path to text.
fs.versionA 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:

CodeMeaning
ENOENTno such file or directory
EEXISTfile already exists
EISDIRillegal operation on a directory
ENOTDIRnot a directory
EPERMoperation 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.

Read more in Guide > Workbench.