Features

Read exactly one symbol, change exactly one symbol

What you get

Your agent reads a function, a method or a type by name, without hunting for line numbers. For a file it gets the outline. Edits are equally targeted: replace one symbol, rename it everywhere, change several files in one patch. Every edit reports back with a receipt, and greppy undo reverses it in one step.

Before and after

First task for serde, a read: Show me ContentDeserializer::deserialize_any.

Without greppy

serde/src/private/de.rs has twelve functions named deserialize_any. The agent finds the right one through its impl block and then cuts it out with sed.

$ grep -n 'impl.*for ContentDeserializer\|fn deserialize_any' serde/src/private/de.rs
38:        fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, E>
1120:    impl<'de, E> Deserializer<'de> for ContentDeserializer<'de, E>
1126:        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
1546:        fn deserialize_any<V>(mut self, visitor: V) -> Result<V::Value, Self::Error>
…
$ sed -n '1126,/^        }$/p' serde/src/private/de.rs
        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
        where
            V: Visitor<'de>,
        {
            match self.content {
                Content::Bool(v) => visitor.visit_bool(v),
…
        }

With greppy

$ greppy read ContentDeserializer::deserialize_any
serde/src/private/de.rs:1126-1154  ContentDeserializer::deserialize_any
        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
        where
            V: Visitor<'de>,
        {
            match self.content {
                Content::Bool(v) => visitor.visit_bool(v),
…
                Content::Map(v) => visit_content_map(v, visitor),
            }
        }
without greppy with greppy
Calls 2 1
Output characters 2,737 1,639
Tokens 696 364

Second task, an edit: Rename get_lit_str to get_str_lit everywhere. The edit ran in a private copy of serde.

Without greppy

The agent checks every match to make sure it really means the function, then replaces by text substitution.

$ grep -rnw get_lit_str --include='*.rs' .
./serde_derive/src/internals/attr.rs:416:                    if let Some(s) = get_lit_str(cx, TAG, &meta)? {
./serde_derive/src/internals/attr.rs:438:                    if let Some(s) = get_lit_str(cx, CONTENT, &meta)? {
…
./serde_derive/src/internals/attr.rs:1411:fn get_lit_str(
…
./serde_derive/src/internals/attr.rs:1545:    let Some(string) = get_lit_str(cx, BORROW, meta)? else {
$ perl -pi -e 's/\bget_lit_str\b/get_str_lit/g' serde_derive/src/internals/attr.rs

With greppy

$ greppy rename get_lit_str get_str_lit
applied serde_derive/src/internals/attr.rs:416,438,490,787,1086,1411,1460,1481,1523,1545  a6f0ae
without greppy with greppy
Calls 2 1
Output characters 1,039 97
Tokens 316 44

Both ways produce a byte-identical file: the definition and nine calls in serde_derive/src/internals/attr.rs are now named get_str_lit. The receipt names every changed line and the edit ID. One command reverses the whole rename:

$ greppy undo
reversed a6f0ae serde_derive/src/internals/attr.rs

How it works

read fetches the definition through the index and prints its current bytes, doc comments and attributes included. On request (--handle), every read command prints a handle that pins the file, the byte range and the content hash. replace-span refuses as soon as the file has changed since. rename changes the definition and every reference the graph resolves, and refuses when the old name still occurs as an identifier without a resolved reference. patch lands a unified diff atomically across all files: either all of them change, or none does. --verify checks the touched files with the matching local checker and reports its diagnostics. Every edit gets an edit ID, and undo reverses it as a whole.

The commands

Command what it does
greppy read S a symbol's definition, byte-exact with doc comments and attributes; for a file, its outline
greppy read-file F --lines A:B exactly the line range A to B of a file, inclusive and 1-based
greppy replace S replace a definition, with --body its body; the new text comes as an argument or through stdin
greppy patch --verify apply a unified diff as one transaction across all files and check the touched files
greppy rename S NAME rename a definition and every graph-resolved reference
greppy undo reverse the last edit, with ID a specific one

More in the docs