GitHub
View as markdown

lets Manual — Target grammar

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:

  1. tree-sitter, for Go, TypeScript, TSX, JavaScript, Python, Rust, Markdown, JSON, YAML, TOML, Shell, C, C++, C#, Java, PHP, Ruby and Swift. .jsonc files resolve #symbol with the JSON grammar. The footer names this resolver via tree-sitter.
  2. A plaintext heuristic, for any other text file. #name finds lines where name is a whole word directly after one of the keywords fn 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 resolver via 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.
  3. @'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.

Back to the top of Target grammar