103 lines
4.0 KiB
Markdown
103 lines
4.0 KiB
Markdown
# Project detection
|
|
|
|
pmacs identifies project roots by walking upward from a file's
|
|
parent directory looking for a marker (`Cargo.toml` for Rust,
|
|
`package.json` for Node, `.git` as a generic VCS fallback, etc.).
|
|
The walk stops at the first match, with language-specific markers
|
|
preferred over generic VCS roots when both exist at the same level.
|
|
|
|
The default behavior matches `git rev-parse --show-toplevel`: walk
|
|
all the way to the filesystem root.
|
|
|
|
## When the default surprises you
|
|
|
|
A file under `/tmp/scratch.rs` will be classified as part of a
|
|
project rooted at `/tmp` if `/tmp/.git` exists — because the walk
|
|
finds the marker before hitting the filesystem root. This is the
|
|
same surprise `git`, `cargo`, and other tools produce; it's
|
|
predictable but occasionally inconvenient.
|
|
|
|
The escape hatch is `pmacs.project.set_search_boundary(path)`. Set
|
|
this in your `init.lua` to clamp the upward walk so a stray marker
|
|
high in the tree cannot capture unrelated files.
|
|
|
|
```lua
|
|
-- Restrict project detection to walk only within ~/code.
|
|
-- Files outside ~/code will not have a project root detected.
|
|
pmacs.project.set_search_boundary(os.getenv("HOME") .. "/code")
|
|
```
|
|
|
|
The boundary is *inclusive*: a marker located at the boundary path
|
|
itself is still found. Set the boundary to the directory that
|
|
contains your projects, not to one level above. To restore the
|
|
default behavior (walk all the way to the filesystem root), pass
|
|
`nil`:
|
|
|
|
```lua
|
|
pmacs.project.set_search_boundary(nil)
|
|
```
|
|
|
|
## Symlinks
|
|
|
|
The boundary applies *after* symlink resolution. When the
|
|
boundary is `/home/user/code` and a search starts from a symlinked
|
|
path that resolves into `/home/user/code/...`, the walk respects
|
|
the boundary correctly.
|
|
|
|
This matters for two common setups:
|
|
|
|
- Corporate `/home` mounts, where `/home/user` may be a symlink to
|
|
`/var/empire/users/user` or similar — the boundary you set
|
|
against your visible home path still works.
|
|
- User-organized symlink farms (e.g., `~/work/foo` linked to
|
|
`~/code/foo`) — search from the symlinked path still terminates
|
|
at the boundary you set against the canonical location.
|
|
|
|
If the boundary path or the search start does not exist on disk,
|
|
canonicalization falls through to the literal path; the comparison
|
|
becomes lexical. This affects pre-creation tests but not normal
|
|
operation.
|
|
|
|
## API summary
|
|
|
|
```lua
|
|
pmacs.project.set_search_boundary(path) -- set, or nil to clear
|
|
pmacs.project.search_boundary() -- current value, or nil
|
|
pmacs.project.detect(file_path) -- honors the boundary
|
|
```
|
|
|
|
`pmacs.project.detect(file_path)` returns
|
|
`{ root, kind, language_id }` for the detected project, or `nil` if
|
|
no marker matches before the boundary (or the filesystem root, when
|
|
no boundary is set).
|
|
|
|
## Design notes
|
|
|
|
We considered several alternatives to the unbounded walk, and
|
|
chose the opt-in boundary as the most predictable:
|
|
|
|
- **Hard-coded stops** at `$HOME` / `/tmp` / mount points break
|
|
legitimate cases (someone's project lives under `/srv/work`,
|
|
someone's `$HOME` is `/var/jeans` over SSH, etc.).
|
|
- **Ownership-based stops** ("walk while same uid as the start
|
|
file") break shared-dev setups and read-only repo mounts, and add
|
|
a `stat` per ancestor.
|
|
- **Confidence-weighted detection** (heuristically score "real
|
|
project-ness") sacrifices the property that makes detection
|
|
useful: predictability.
|
|
|
|
Matching `git`'s behavior keeps detection's failure mode consistent
|
|
with the rest of the user's toolchain. The boundary gives users
|
|
who care a precise, configurable opt-in without imposing a
|
|
specific policy on everyone.
|
|
|
|
## Forward planning
|
|
|
|
The boundary is workspace-scoped (one boundary per `Workspace`
|
|
instance). When project-local `init.lua` lands (post-v0.1) we may
|
|
extend this to a per-project boundary, or to a stack of boundaries
|
|
that nested project loads can push and pop. The v0.1 surface is
|
|
deliberately minimal so those future extensions don't break
|
|
existing user config: setting a single workspace-wide boundary in
|
|
your global `init.lua` continues to do exactly what it does today.
|