Skip to content

clojure_edit replace reformats entire file via cljfmt, not just the target form causing Noisy diffs and Stale REPL vars #154

@williamp44

Description

@williamp44

clojure_edit replace reformats entire file via cljfmt, not just the target form

Summary

When using clojure_edit with operation: "replace" to replace a single top-level form, the pipeline applies cljfmt to the entire file — not just the replaced form. This causes collateral whitespace changes in unrelated code throughout the file causing Noisy diffs and Stale REPL vars

Steps to reproduce

  1. Create a Clojure file with intentional alignment spacing:
(ns example.core)

(def ob-yes-heavy
  {:orderbook {:yes [[60 200] [55 150]]
               :no  [[40 80]  [35 60]]}})

(def ob-empty-no  {:orderbook {:yes [[60 100]] :no []}})
(def ob-empty     {:orderbook {:yes [] :no []}})

(defn foo [x] (+ x 1))

(defn bar [x] (+ x 2))
  1. Use clojure_edit to replace only the bar function:
file_path: "example.clj"
form_type: "defn"
form_identifier: "bar"
operation: "replace"
content: "(defn bar [x] (+ x 3))"
  1. Observe the diff — changes appear outside the replaced form:
 (def ob-yes-heavy
   {:orderbook {:yes [[60 200] [55 150]]
-               :no  [[40 80]  [35 60]]}})
+               :no [[40 80] [35 60]]}})

-(def ob-empty-no  {:orderbook {:yes [[60 100]] :no []}})
-(def ob-empty     {:orderbook {:yes [] :no []}})
+(def ob-empty-no {:orderbook {:yes [[60 100]] :no []}})
+(def ob-empty {:orderbook {:yes [] :no []}})

 (defn foo [x] (+ x 1))

-(defn bar [x] (+ x 2))
+(defn bar [x] (+ x 3))

Only the bar defn should have changed.

Root cause

In pipeline.clj, the edit-form-pipeline function:

  1. Locates the target form via find-top-level-form
  2. Replaces it with z/replace (scoped to the single form) ✅
  3. Converts the zipper back to a string ✅
  4. Applies format-source (cljfmt) to the entire file string ← problem

The default cljfmt options in core.clj include aggressive normalizers:

{:remove-surrounding-whitespace? true
 :remove-trailing-whitespace? true
 :remove-consecutive-blank-lines? true
 :insert-missing-whitespace? true
 :remove-multiple-non-indenting-spaces? true}

These rewrite whitespace across the entire file, not just the replaced region.

Impact

For Claude Code / AI agent workflows, this is particularly disruptive:

  • Noisy diffs: A single-form replacement produces a diff with dozens of unrelated whitespace changes, making review harder.
  • Stale REPL vars: When cljfmt reformats forms surrounding a replaced deftest, Babashka's require :reload doesn't remove old vars that were defined at now-shifted line positions. This causes phantom test failures from ghost test vars that no longer exist in the file but persist in the REPL namespace.

Suggested fix

Any of these would resolve it:

  1. Scope cljfmt to only the replaced form — format the new content before inserting it, rather than formatting the whole file after.
  2. Make whole-file formatting opt-in — default to formatting only the replaced form; add a config flag like :format-whole-file? true for users who want it.
  3. Respect the file's existing style — skip formatting entirely when the operation is replace (the user's provided content is already how they want it).

Workaround: Users can set :cljfmt false in their MCP config to disable the formatting step entirely.

Environment

  • clojure-mcp v0.2.6 (commit 35a660b)
  • Babashka nREPL
  • Claude Code with :cli-assist config profile

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions