# Rename a symbol across the workspace

> edit_rename_symbol is the safe alternative to grep + per-file text replace for cross-file renames. It uses the typed symbol graph from std/code_librarian to resolve the seed...

Website: https://harnlang.com/cookbooks/rename-symbol.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

`edit_rename_symbol` is the safe alternative to grep + per-file text replace
for cross-file renames. It uses the typed symbol graph from
[`std/code_librarian`](../stdlib/code-librarian.md) to resolve the seed symbol,
walks every file in scope with tree-sitter to find identifier-context
occurrences (skipping comments and string literals), and refuses to write if
the new name already exists as an identifier in any rewritten file.

When the host runs with a staged-fs session (`harness.fs.set_mode`), every
touched file lands in the overlay; one `harness.fs.commit_staged` flips them
atomically. Without a session, the host still buffers the full plan in memory
and only writes after pre-flight passes, so a clean run is all-or-nothing.

Supported languages (first batch): Harn, Rust, TypeScript/TSX, JavaScript/JSX,
Python, Swift, Go.

## Recipe — rename a Rust struct across the workspace

The workspace defines `Widget` in one file and uses it from another. We
rename it to `Gadget` with a single call, then commit the staged writes.

```harn,ignore
import { edit_rename_symbol } from "std/edit"

pipeline rename_widget_to_gadget(harness: Harness, session_id: string) {
  // Workspace must already be indexed; usually the host does this on
  // startup. From a script you can run
  // `harness.code_index.rebuild` first.
  harness.fs.set_mode({ session_id: session_id, mode: "staged" })

  const plan = edit_rename_symbol(harness.code_index,
    harness.code_index,
    {
      symbol_ref: { name: "Widget", path: "src/lib.rs", kind: "Type" },
      new_name: "Gadget",
      scope: "workspace",
      session_id: session_id,
      dry_run: true,
    },
  )

  if !plan.ok {
    harness.stdio.println(
      "rename refused: " + plan.result + " — " + (plan.details ?? ""),
    )
    return plan
  }

  // Review the staged plan before committing. `touched_files[*].edits[*]`
  // exposes byte and (row, col) spans on both sides of the edit.
  for file in plan.touched_files {
    harness.stdio.println(
      "would rewrite " + file.path
        + " (" + str(len(file.edits)) + " edits)",
    )
  }

  // Drop dry_run to actually stage the writes, then commit.
  const applied = edit_rename_symbol(harness.code_index,
    harness.code_index,
    {
      symbol_ref: { name: "Widget", path: "src/lib.rs", kind: "Type" },
      new_name: "Gadget",
      scope: "workspace",
      session_id: session_id,
    },
  )
  if !applied.ok { return applied }

  return harness.fs.commit_staged({ session_id: session_id })
}
```

## Conflict simulation

If `Gadget` already exists as an identifier in any file the rename would
touch, the host short-circuits with `result: "conflict"` and never writes:

```harn,ignore
import { edit_rename_symbol } from "std/edit"

pipeline rename_would_shadow(harness: Harness) {
  // `src/main.rs` defines both `Widget` and `Gadget`. Renaming Widget
  // to Gadget would create two `Gadget` definitions in the same file —
  // the host rejects before touching disk and surfaces the shadow site.
  const result = edit_rename_symbol(harness.code_index,
    harness.code_index,
    {
      symbol_ref: { name: "Widget", path: "src/main.rs", kind: "Type" },
      new_name: "Gadget",
      scope: "workspace",
    },
  )

  assert(result.result == "conflict")
  for site in result.conflicts {
    harness.stdio.println(
      "shadow at " + site.path
        + ":" + str(site.row + 1) + ":" + str(site.col + 1),
    )
  }
  return result
}
```

## Result shape

`result` is one of:

| `result`                | meaning                                                       |
|-------------------------|---------------------------------------------------------------|
| `"applied"`             | rename succeeded (or, with `dry_run`, would have).            |
| `"conflict"`            | `new_name` shadows an existing identifier in a rewritten file.|
| `"no_match"`            | `symbol_ref` did not resolve in the typed graph.              |
| `"ambiguous_symbol"`    | multiple symbols share `symbol_ref.name`; pass `line`/`kind`. |
| `"unsupported_language"`| an in-scope file uses a grammar outside the first batch.      |
| `"invalid_identifier"`  | `new_name` is not a valid identifier token.                   |
| `"syntax_error"`        | a rewritten file failed re-parse with `validate=true`.        |

Each entry in `touched_files` carries the workspace-relative `path`, the
detected `language`, `before_sha256` / `after_sha256` (over the full file
body, so the caller can detect concurrent edits), and an `edits` list of
`{start_byte, end_byte, start_row, start_col, end_row, end_col, before,
after}` per occurrence.

For the surface reference see [`std/edit`](../stdlib/edit.md); for the
underlying graph see
[`std/code_librarian`](../stdlib/code-librarian.md).

---

## Read next

- [Pool cookbook](https://harnlang.com/cookbooks/pools.md)
- [Structured refactorings cookbook](https://harnlang.com/cookbooks/structured-refactorings.md)
