filesystem

package
v1.63.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const MaxEditableBytes = 16 << 20 // 16 MiB

MaxEditableBytes caps how large a file may be to be read into the editor.

View Source
const MaxListedFiles = 5000

MaxListedFiles caps the recursive file list ListFilesUnder returns.

View Source
const MaxSearchMatches = 200

MaxSearchMatches caps how many matching lines a project search returns. This is a limit on the answer, not on the search: every file is scanned, and this only decides how much of the result travels to the browser.

View Source
const QuickOpenLimit = 100

QuickOpenLimit caps how many matches one quick open query returns. The palette never renders more than this, so finding more is wasted work.

Variables

View Source
var (
	ErrTooLarge = errors.New("File is too large to edit in the browser.")
	ErrBinary   = errors.New("Binary files cannot be edited.")
)

ErrTooLarge and ErrBinary mark files the browser editor cannot edit; callers can offer a viewer or a download instead of a plain error.

View Source
var (
	ErrFileChanged = errors.New("The file changed on disk after it was opened.")
	ErrFileDeleted = errors.New("The file no longer exists on disk.")
)

ErrFileChanged and ErrFileDeleted are the two ways a versioned write is refused, and they are told apart because the ways out are not the same: a changed file can be read back over the buffer, a deleted one cannot and can only be written again as a new file. Neither of them ever writes.

View Source
var DefaultExclusions = []string{".git"}

DefaultExclusions are what applies when nothing has been configured: only git's own storage, which is never something you open in an editor and whose loose object files can outnumber the project itself.

Dependency folders are deliberately not in here. They are code you do read, stepping into a library to see what it does is a normal thing to want, and the index carries them without trouble. What they cost is relevance rather than time, so excluding them is a choice each project makes on the search settings.

View Source
var ErrExists = errors.New("A file or folder with that name already exists.")

ErrExists reports a name that is already taken. The editor answers it with a 409 so the browser can offer to overwrite instead of showing a dead end.

Functions

func AbsDir

func AbsDir(p string) string

AbsDir spells a directory the one way everybody who derives something from it has to spell it: a command may name it relative or with a ~, and two spellings of one directory must not become two answers. It is what the unix socket paths are derived from, where a relative path would be resolved against whatever working directory the reader happens to have, and where the length of the path decides whether it fits an address at all.

A path that cannot be expanded is answered as it came: nothing here refuses, the caller's own step does that if it has to.

func ArchiveExt

func ArchiveExt(name string) (string, bool)

ArchiveExt reports whether the name looks like an archive the editor can unpack, and returns the name without that extension.

func CheckEditableText

func CheckEditableText(data []byte) error

CheckEditableText rejects content the browser editor cannot handle: anything over MaxEditableBytes and binary data (containing a NUL byte).

func CleanBaseName

func CleanBaseName(raw string) (string, error)

func DirSignature added in v1.60.0

func DirSignature(entries []Entry) string

DirSignature is the token of a directory listing: which entries it holds and which of them are folders, in the order ListDir answers them, which is the order the tree renders. It goes out with every listing so a client can hand it back and be told whether what it is showing is still the disk. Sizes and timestamps stay out on purpose: a write into a file it holds changes neither a row nor this.

func ExpandHome

func ExpandHome(p string) (string, error)

func HomeDir

func HomeDir() (string, error)

func HumanSize

func HumanSize(n int64) string

HumanSize formats a byte count for display; negative counts are "unknown".

func IsUnder

func IsUnder(path, root string) bool

IsUnder reports whether path is root itself or inside root.

func ListFilesUnder

func ListFilesUnder(root, rel string, ex Exclusions) (files []string, truncated bool, err error)

ListFilesUnder walks one directory inside root and answers the relative paths of every regular file in it. An empty rel walks the whole project.

The excluded directories are skipped, symlinked directories are not followed, and the walk stops after MaxListedFiles files and reports truncation. The paths stay relative to root, which is what every client path is, so a caller that only cares about a subtree pays for that subtree and the cap counts there.

The quick open palette does not use this: it answers from an index of the whole project instead, because a capped list cannot find what sits past the cap. See QuickOpenCache.

func ReadFileText

func ReadFileText(root, rel string) (content string, version string, err error)

ReadFileText reads a regular file for editing and answers the version token of exactly the bytes it returns, which is what the save carries back. The token is taken from those bytes and never from a second look at the disk: a write landing between the read and that look would hand the caller a token for content it never saw. It rejects directories, files over MaxEditableBytes, and binary content.

func ResolveExistingFile

func ResolveExistingFile(root, rel string) (string, os.FileInfo, error)

ResolveExistingFile resolves rel to an existing regular file under root and returns its absolute path along with its file info.

func ResolveUnder

func ResolveUnder(root, rel string) (string, error)

ResolveUnder cleans a slash-separated, client-supplied relative path and joins it onto root, refusing anything that would escape root (lexically or via symlinks). An empty rel resolves to root itself.

func SnippetAround

func SnippetAround(text string, idx int) string

SnippetAround trims a line for transport, keeping a window around the byte offset idx when the line is longer than the snippet cap. The search cuts around its match and the LSP usage previews around the usage's column, so the two lists that share one look share one cutting rule; idx counts into the given text, leading whitespace included, and is clamped.

func ToDirectoryName

func ToDirectoryName(raw string) string

ToDirectoryName normalizes user input to a lowercase alnum-dash slug. Invalid characters collapse to '-', and invalid/empty inputs become "".

func WriteTarGz

func WriteTarGz(w io.Writer, dir, prefix string) error

WriteTarGz streams dir as a gzipped tar, with prefix as the single top level folder inside the archive. Only directories and regular files go in, anything else (symlinks, sockets) is skipped so the archive stays inside the project.

Types

type EditorConfig

type EditorConfig struct {
	IndentStyle string `json:"indentStyle,omitempty"` // "tab" or "space"
	IndentSize  int    `json:"indentSize,omitempty"`  // columns; 0 when unset or "tab"
	TabWidth    int    `json:"tabWidth,omitempty"`
}

EditorConfig holds the indentation properties EditorConfig resolves for one file. A zero field means the property is unset, so the client keeps its own default for it.

func EditorConfigForFile

func EditorConfigForFile(root, rel string) EditorConfig

EditorConfigForFile resolves the EditorConfig indentation properties that apply to root/rel by walking the .editorconfig cascade upward from the file. A missing, unreadable or malformed config yields a zero EditorConfig, never an error: editorconfig is advisory and must not block opening a file.

type Entry

type Entry struct {
	Name     string `json:"name"`
	RelPath  string `json:"path"`
	IsDir    bool   `json:"isDir"`
	Size     int64  `json:"size"`
	SizeText string `json:"sizeText"`
	ModTime  string `json:"modTime"`
}

Entry is one item (file or directory) inside a project tree. RelPath is the slash-separated path relative to the tree root and is what the client sends back to identify the item.

func CopyEntry

func CopyEntry(root, rel, dirRel string, overwrite bool) (Entry, error)

CopyEntry copies the file or directory at root/rel into the directory root/dirRel. Copying into its own folder duplicates the entry under a free "name copy" name; anywhere else a taken name is ErrExists unless overwrite replaces the file there.

func CreateDir

func CreateDir(root, rel string) (Entry, error)

CreateDir creates a directory at root/rel (and any missing parents). It fails if the path already exists.

func CreateFile

func CreateFile(root, rel string) (Entry, error)

CreateFile creates an empty regular file at root/rel, creating any missing parent directories. It fails if the path already exists.

func DeleteEntry

func DeleteEntry(root, rel string) (Entry, error)

DeleteEntry removes a file or directory (recursively) under root. It refuses to delete the root itself.

func ExtractArchive

func ExtractArchive(root, rel string) (Entry, error)

ExtractArchive unpacks root/rel into a new folder next to it, named after the archive. The folder name is free by construction, so nothing is overwritten. Only directories and regular files are written; anything else in the archive (symlinks, devices, paths that would leave the folder) is skipped.

func ListDir

func ListDir(root, rel string) ([]Entry, error)

ListDir returns the directories and regular files directly inside root/rel, directories first, then files, each ordered case-insensitively by name.

func MoveEntry

func MoveEntry(root, rel, dirRel string, overwrite bool) (Entry, error)

MoveEntry moves the file or directory at root/rel into the directory root/dirRel, keeping its base name. An empty dirRel is the project root. Without overwrite a taken name is reported as ErrExists, with it the file there is replaced.

func RenameEntry

func RenameEntry(root, rel, newName string) (Entry, error)

RenameEntry renames the file or directory at root/rel to newName inside the same parent directory. newName must be a bare name without path separators.

func SaveUpload

func SaveUpload(root, dirRel, filename string, src io.Reader, overwrite, createDirs bool) (Entry, error)

SaveUpload stores an uploaded file as dirRel/filename under root, writing to a temp file first and renaming into place. A taken name is reported as ErrExists unless overwrite replaces the file there; createDirs makes the missing folders on the way, which is what a folder upload needs.

func WriteFileText

func WriteFileText(root, rel string, content []byte) (Entry, error)

WriteFileText writes content to root/rel atomically, preserving the existing file mode when the target already exists. The parent directory must exist. It writes whatever is there: a caller that has to keep somebody else's work goes through WriteFileTextIfUnchanged.

func WriteFileTextIfUnchanged

func WriteFileTextIfUnchanged(root, rel string, content []byte, version string) (Entry, string, error)

WriteFileTextIfUnchanged writes content to root/rel, but only while the file on disk is still the one version was taken from. A file somebody else wrote in the meantime is ErrFileChanged and a file that is gone is ErrFileDeleted; both leave the disk exactly as it is. The version of what was written comes back with the entry, so a second save right after the first asks nothing.

An empty version is the create path and writes whatever is there. That is what a file created in the editor takes on its first save, before anything ever read it back, and it is what the answer to a deleted file is: writing the buffer as a new file is a decision somebody made in front of the dialog, not a save that overwrote something silently.

type Exclusions

type Exclusions struct {
	// contains filtered or unexported fields
}

Exclusions are the directories a recursive walk stays out of: version control internals, vendored dependencies, build output. They are configured per install on the editor's search settings, because which folders are noise is a property of the project, not something the editor can know.

Two kinds of entry:

  • a bare name like "vendor" or "node_modules" excludes every directory with that name, at any depth
  • a path like "tests/_output" excludes exactly that directory, relative to the project root

Excluding a directory keeps the walk out of it entirely, so nothing below it is indexed or searched either.

func DefaultExclusionSet

func DefaultExclusionSet() Exclusions

DefaultExclusionSet is ParseExclusions over DefaultExclusions.

func ParseExclusions

func ParseExclusions(raw string) Exclusions

ParseExclusions reads a stored or submitted list. Entries are separated by newlines or commas; surrounding whitespace and slashes are trimmed, blank entries and duplicates are dropped. Anything that would exclude the project root itself is ignored, because a walk of nothing is never what someone meant.

func (Exclusions) Equal

func (e Exclusions) Equal(other Exclusions) bool

Equal reports whether two sets exclude the same things. The quick open index uses it to notice that the setting changed and rebuild.

func (Exclusions) Len

func (e Exclusions) Len() int

Len reports how many entries there are.

func (Exclusions) List

func (e Exclusions) List() []string

List returns the entries in a stable order, names first, then paths, each alphabetically. It is what the settings form shows and what gets stored, so the value a person sees is the value that applies.

func (Exclusions) SkipDir

func (e Exclusions) SkipDir(rel, name string) bool

SkipDir reports whether a walk should stay out of the directory at the given root-relative path. name is its base name.

func (Exclusions) String

func (e Exclusions) String() string

String is the canonical stored form: one entry per line.

type ExtensionCount added in v1.60.0

type ExtensionCount struct {
	Pattern string `json:"pattern"`
	Files   int    `json:"files"`
}

ExtensionCount is one file name pattern of a project and how many files carry it, written the way the mask takes it.

type File

type File struct {
	Name     string
	Path     string
	Size     int64
	SizeText string
	ModTime  string
}

func DeleteFile

func DeleteFile(dir, rawName string) (File, error)

func ListFiles

func ListFiles(dir string) ([]File, error)

func SaveFile

func SaveFile(dir, rawName string, src io.Reader) (File, error)

type FileMask added in v1.60.0

type FileMask struct {
	// contains filtered or unexported fields
}

FileMask is the file name filter of a content search: a comma separated list of glob patterns, matched case insensitively, with * and ? as the wildcards.

A pattern without a slash is matched against the file's base name, so "*.go" means the extension anywhere in the tree; a pattern with one is matched against the project relative path. A leading "!" turns a pattern into an exclusion: the inclusions are an or, the exclusions then take files back out again, and a mask holding nothing but exclusions includes everything else.

The zero value lets every file through, which is what a search without a mask asks for.

func ParseFileMask added in v1.60.0

func ParseFileMask(raw string) FileMask

ParseFileMask reads what the palette's file field holds. Whitespace around a pattern is trimmed and blank entries are dropped, so the trailing comma standing there while the next pattern is typed does not empty the answer.

func (FileMask) Empty added in v1.60.0

func (m FileMask) Empty() bool

Empty reports whether the mask lets every file through. A search asks this before it matches anything, because a mask nobody typed should cost nothing.

func (FileMask) Match added in v1.60.0

func (m FileMask) Match(rel string) bool

Match reports whether the file at the project relative path rel passes the mask.

type FilterFacts added in v1.60.0

type FilterFacts struct {
	Folders    []FolderCount    `json:"folders"`
	Extensions []ExtensionCount `json:"extensions"`
	Files      int              `json:"files"`
}

FilterFacts is what a project offers the palette's two filters: the folders to scope to and the file name patterns that actually occur in it. Both fall out of the quick open index, so the choices cost no walk of their own.

type FolderCount added in v1.60.0

type FolderCount struct {
	Path  string `json:"path"`
	Files int    `json:"files"`
}

FolderCount is one folder of a project and how many files sit under it, at any depth. The scope is recursive, so that is the number that says what picking this folder would cover.

type OpenedFile

type OpenedFile struct {
	File
	io.ReadCloser
}

func OpenFile

func OpenFile(dir, rawName string) (OpenedFile, error)

type QuickOpenCache

type QuickOpenCache struct {
	// contains filtered or unexported fields
}

QuickOpenCache holds one index per project. Queries are answered from the index that is there, so only the very first query of a project waits for a walk. An index the editor itself invalidated is rebuilt on the next query; an index that merely went stale is served as it is and refreshed in the background.

func NewQuickOpenCache

func NewQuickOpenCache() *QuickOpenCache

NewQuickOpenCache returns an empty cache.

func (*QuickOpenCache) Facts added in v1.60.0

func (c *QuickOpenCache) Facts(root string, ex Exclusions) (FilterFacts, error)

Facts answers what the palette's filters can be set to in the project at root. It is the same lookup Query makes with a different question at the end.

func (*QuickOpenCache) Forget

func (c *QuickOpenCache) Forget(root string)

Forget drops root from the cache entirely. Invalidate is for a project that changed and will be queried again, so it keeps the entry and only clears the index; a project that is gone has no next query, and the sweep that would have collected it runs on a query. Its index would sit there until somebody happened to search in another project.

func (*QuickOpenCache) Invalidate

func (c *QuickOpenCache) Invalidate(root string)

Invalidate drops the index for root so the next query rebuilds it. The editor calls this after it changed the tree itself, which is what makes a file findable immediately after being created.

func (*QuickOpenCache) Query

func (c *QuickOpenCache) Query(root, query, scope string, ex Exclusions, limit int) (QuickOpenMatches, error)

Query answers a quick open query for the project at root, building or refreshing the index as needed.

type QuickOpenMatches

type QuickOpenMatches struct {
	Paths   []string
	Total   int
	Indexed int
}

QuickOpenMatches is one answer to a quick open query: the paths to show and how many candidates there were in total, so the palette can say that it is showing only the first few.

type ReplaceMatch added in v1.60.0

type ReplaceMatch struct {
	Path       string `json:"path"`
	Line       int    `json:"line"`
	Text       string `json:"text"`
	MatchStart int    `json:"start"`
	MatchLen   int    `json:"len"`
	After      string `json:"after"`
	AfterStart int    `json:"afterStart"`
	AfterLen   int    `json:"afterLen"`
}

ReplaceMatch is one line a replacement would change: the line as it stands and as it would read, each marked where the change sits, in UTF-16 units like the search marks its hits.

type ReplaceReport added in v1.60.0

type ReplaceReport struct {
	Matches   []ReplaceMatch `json:"matches"`
	Truncated bool           `json:"truncated"`
	Total     int            `json:"total"`
	Files     int            `json:"files"`
	Replaced  int            `json:"replaced"`
	Changed   []string       `json:"changed"`
	Blocked   []string       `json:"blocked"`
}

ReplaceReport is what a replacement would do, or did. Total and Files count every occurrence in the whole scope, not only the ones the preview carries: the button says what pressing it costs, and a capped list must never be mistaken for the size of the job.

func ApplyReplace added in v1.60.0

func ApplyReplace(root string, req ReplaceRequest, ex Exclusions) (ReplaceReport, error)

ApplyReplace performs the job. It reads the whole scope first and writes nothing until it knows that no file it would touch is held unsaved in the browser, so a refusal leaves the project exactly as it was.

func PreviewReplace added in v1.60.0

func PreviewReplace(root string, req ReplaceRequest, ex Exclusions) (ReplaceReport, error)

PreviewReplace answers what the job would change without touching anything: the first MaxSearchMatches lines with their before and after, and the whole count behind them.

type ReplaceRequest added in v1.60.0

type ReplaceRequest struct {
	Query       string
	Replacement string
	UseRegex    bool
	Options     SearchOptions
	// OnlyPath and OnlyLine narrow the job to the occurrences on one line of
	// one file, which is what a single row's own replace asks for.
	OnlyPath string
	OnlyLine int
	// Dirty are project relative paths the browser holds unsaved. A job that
	// would touch one of them writes nothing at all: the file on disk and the
	// buffer would otherwise part ways without anybody being told.
	Dirty []string
}

ReplaceRequest is one replacement job: what to find, what to put in its place, and where to look. The scope is the search's own, so a replacement can only ever reach what the same query, folder and mask showed.

type SearchMatch

type SearchMatch struct {
	Path       string `json:"path"`
	Line       int    `json:"line"`
	Text       string `json:"text"`
	MatchStart int    `json:"start"`
	MatchLen   int    `json:"len"`
}

SearchMatch is one matching line of a project wide text search. MatchStart and MatchLen locate the hit inside Text, in UTF-16 units, the coordinate the browser slices strings in, so the palette can mark the hit by position instead of searching the snippet again.

func SearchFiles

func SearchFiles(root, query string, useRegex bool, ex Exclusions, opt SearchOptions) ([]SearchMatch, bool, error)

SearchFiles scans every regular file under root for a case insensitive substring match, or with useRegex for a case insensitive RE2 match, staying out of the excluded directories and ignoring binary and oversized files.

It used to stop after the first 5000 files as well as after 200 matches, which meant a search could report "no matches" while the file it was looking for sat unread a few thousand entries further down. In a large project, searching for a symbol in your own code spent the whole budget inside generated code and answered, quickly and confidently, that nothing matched. The file limit is gone; only the answer is capped.

Files are read on a pool of workers because the scan is bound by reading tens of thousands of files rather than by matching bytes. The result is still the first MaxSearchMatches matches in path order, exactly what a single threaded in-order scan would have returned.

opt narrows what is searched without changing what comes back: the paths stay relative to the project root even when the walk starts in a folder below it, so a match names the same file whether or not a scope was set.

type SearchOptions added in v1.60.0

type SearchOptions struct {
	Folder string
	Mask   FileMask
	// CaseSensitive turns off the case folding both kinds of match do by
	// default. The zero value is what the palette has always done.
	CaseSensitive bool
}

SearchOptions narrows a search below the project. Folder is a project relative directory the walk stays inside, empty meaning the whole project, and Mask decides which files are read at all. The zero value searches every file under the project root, which is what a request carrying neither of the two asks for.

type Stamp added in v1.60.0

type Stamp struct {
	// Exists is false for everything the editor cannot answer for: a path that
	// is gone, one that is not the kind of thing it was watched as, and one
	// that escapes the project. All of those are a movement the tab or the tree
	// has to hear about, and none of them is an error worth its own case.
	Exists  bool
	Size    int64
	ModTime int64
	// Version is the token: a file's content hash, a directory's listing
	// signature. A file too large to read carries none, and where it is empty
	// the stat is all there is and Same falls back to it.
	Version string
}

A stamp is what one round of the editor's file watch knows about one path, and it exists because the two questions that watch asks are not one question. Whether a file's content moved is the version token's answer, the same token the save already stands on (see version.go). Whether a directory's set of entries moved is that directory's listing, which is what the tree renders.

The stat is the prefilter and never the decision. A path whose size and mtime are where the round before left them is neither read nor listed and keeps the token it had; only one whose stat moved is looked at, and the token says whether anything really happened. That the stat lies in both directions is precisely why it may not decide: a git checkout rewrites identical bytes and moves the timestamp, so deciding on the stat would report a change to everybody for nothing, and a coarse kernel clock cannot tell two writes inside one tick apart. A directory's mtime is the sharper of the two, it moves on a create, a delete and a rename inside it and on nothing else, which is exactly the semantics a lazily loaded tree needs and why a folder is no special case: a folder is an entry in its parent like every other one. Even there it is only the prefilter, because a timestamp cannot be compared with what a browser is showing and a listing can.

That comparison is the point of the token being what it is. A stamp can be seeded from what a client says it holds, the version of the file it read or the signature of the listing it rendered, and the next round then answers "the disk is not what you are showing" instead of "nothing moved since I started looking". Without that, a path that joins the watch and is written a moment later is written into the very first reading of it and is never reported at all.

func SeedStamp added in v1.60.0

func SeedStamp(token string) Stamp

SeedStamp is a stamp taken from what a client says it holds rather than from the disk. Its stat is deliberately empty, so the next round cannot take the prefilter's word for it and has to look: that look is the comparison between the disk and the screen.

func StampDir added in v1.60.0

func StampDir(root, rel string, last Stamp) Stamp

StampDir probes one directory: the mtime says whether to look, the listing says what is there. Listing costs a ReadDir, which is why it only happens where the mtime moved, and that is exactly where something appeared, disappeared or was renamed.

func StampFile added in v1.60.0

func StampFile(root, rel string, last Stamp) Stamp

StampFile probes one file, reading it only when the stat says it moved since last. A file that is gone, is not a regular file or lies outside the project answers the zero stamp, which is what a deleted file looks like to the tab that has it open.

The read is not ReadFileText: what is watched here is whatever a tab holds, and a tab holds images and archives too. Binary content is a token like any other, only the size limit stands, and past it the stat is the token.

func (Stamp) Same added in v1.60.0

func (s Stamp) Same(other Stamp) bool

Same reports whether two stamps describe the same state. The token decides wherever there is one, and the stat decides where there is none, which is what makes one type serve a file and a directory alike.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL