Target grammar
One grammar, spoken by show, edit and transform. find takes a plain path or directory,
not a target, but every find hit prints as path:line, which is itself a valid target for a
following show or edit call.
| Form | Means |
|---|---|
path |
whole file, windowed to --window lines (show) |
path:40 |
one line; add -A/-B/-C for context on show |
path:40-80 |
a line range; edit accepts a range only with --expect, --expect-all or --if |
path@'regex' |
the first line matching the regex, with -A/-B/-C context; path@'regex'+2 is the second match |
path#name |
the enclosing symbol: function, method, type, class, or a markdown heading |
path#Outer.inner |
a nested symbol |
File names win over metacharacters
A target string that names an existing file, whole, is a path — checked before any
metacharacter is parsed. lets show 'C#.md' opens the file C#.md; only when no such file
exists is # read as the symbol separator. Verified:
$ lets show 'C#.md'
── C#.md (1-3 of 3) · sha:[..]
1 one
2 two
3 three
── showed 1 target · 3 lines
$ lets show 'C#.md:2'
── C#.md:2 (2-2 of 2) · sha:[..]
2 b
── showed 1 target · 1 line
When the whole string is not an existing file, lets looks for the longest prefix that ends
right before a #, @ or :, names an existing file, and leaves a well-formed suffix
(#name, @'regex'[+N], :N or :N-M). Only that remainder is read as the suffix. When no
prefix qualifies, the string parses by metacharacter as the table above shows, so a.rs#main is
the symbol main in a.rs.
Symbol resolution (#name)
Tried in order:
- tree-sitter, for Go, TypeScript, TSX, JavaScript, Python, Rust, Markdown, JSON, YAML,
TOML, Shell, C, C++, C#, Java, PHP, Ruby and Swift.
.jsoncfiles resolve#symbolwith the JSON grammar. The footer names this resolvervia tree-sitter. - A plaintext heuristic, for any other text file.
#namefinds lines wherenameis a whole word directly after one of the keywordsfn func function def class struct interface enum type fun trait impl object module sub proc record const let val var, or directly before an opening(. The span is found by brace matching first, then by indentation, then falls back to the single matching line. The footer names this resolvervia heuristic (plaintext). Kotlin (.kt) always uses this path — there is no tree-sitter grammar crate for it that both passes the project’s compatibility test and builds. @'regex'is always available as a fallback for any file.
More than one candidate line for a symbol is exit 2 with every candidate listed as path:line;
no candidate is exit 1 with a hint to use @'regex' instead.
$ lets show 'store.go#Open'
? 2
store.go#Open is ambiguous (2 candidates)
store.go:44 func Open(path string) (*Store, error) {
store.go:213 func (s *Store) Open(ctx context.Context) error {
ERROR_CODE=ambiguous
$ lets show 'greet.kt#greet'
── greet.kt#greet (3-5 of 7 · via heuristic (plaintext)) · sha:[..]
3 fun greet(name: String): String {
4 return "hi $name"
5 }
── showed 1 target · 3 lines
Because the end of a plaintext span is a guess rather than a parsed boundary, edit --insert-after on a plaintext symbol is refused (ERROR_CODE=guessed_span) unless the span came
from brace matching; --insert-before is unaffected, since it only needs the start.
A target that doesn’t parse gets a diagnosis, not a bare “not found”
lets recognizes four common habits typed against a file that does exist, and suggests the form
it would have accepted, exit 1 ERROR_CODE=not_found:
| Typed | Reads as | Suggestion |
|---|---|---|
path:40,60 |
the sed comma habit |
path:40-60 |
path:40:60 |
the colon habit / grep’s file:line:col |
path:40-60 |
path:name |
— | path#name |
path@word (shell stripped the quotes) |
— | path@'word' |
$ lets show a.ts:40,60
? 1
a.ts:40,60: no such file · did you mean a.ts:40-60
ERROR_CODE=not_found
$ lets show a.ts@cap
? 1
a.ts@cap: no such file · did you mean "a.ts@'cap'"
ERROR_CODE=not_found
When no prefix of the string names a file at all, the message is the plain one: nope.ts: No such file or directory (os error 2).
A directory given as a target is not a target: exit 7 ERROR_CODE=unsupported_file, naming
lets find as the way to search it.
@'regex' with no match falls back to find’s reading
A @'regex' that matches no line is retried with the same grep-style second reading find gives
a pattern that matched nothing (see lets find): show f.go@'A\|B' shows the first line
matching A|B, and the footer names the reading used.
find output is made of targets
Every find hit prints as path:line, so the next call needs no read — pass that string
straight to show or edit.