#4604 · @micuintus · opened Nov 21, 2025 at 6:47 PM UTC · last updated Mar 21, 2026 at 9:09 AM UTC
feat(formater): restrict formatting to only the changed range of a file.
Score breakdown
Impact
Clarity
Urgency
Ease Of Review
Guidelines
Readiness
Size
Trust
Traction
Summary
This PR aims to restrict clang-format to only the edited line range, reducing diff noise from unrelated formatting changes. However, it contains an unexplained, unrelated breaking API change and is very large for a feature of this nature.
Description
Restrict formatting only to the edited line range for clang-format When using the Edit tool, clang-format now only formats the specific lines that were changed, rather than reformatting the entire file. This prevents unrelated formatting changes from cluttering the diff.
Closes #4603
Linked Issues
#4603 [FEATURE]: Restrict formatting only to the edited range for clang-format (and Prettier)
View issueComments
PR comments
micuintus
@rekram1-node What is the state on this? Are you considering these suggestions?
rekram1-node
Yeah I see what you are trying to solve there. I would need some time to look into it more to review but I will say this looks very vibe coded so I won't just auto merge without a throughout review
micuintus
@rekram1-node
I would need some time to look into it more to review but I will say this looks very vibe coded so I won't just auto merge without a throughout review
Yes, I did use opencode for this PR ;). I am obviously new to this codebase and not a particular TypeScript expert. I had opencode run through several iterations with several models (Sonnet 4.5, GLM 4.6, Gemini 3, CODEX) though, letting the models review and discuss their results; asked them to asses the solution and the code quality, find bugs, make suggestions for simplification and improvement etc.
But yeah, this code needs to be reviewed, of course :)
I started with the line-number-based range API of clang-format and actually left it as a separate commit (the rest is squashed into the subsequent commit). Then I checked which interface Qt Creator uses: char/byte range API for subfile formatting --- and as it enables us to apply the same logic with Prettier I thought this makes sense as a "source of truth" format.
Aside from my use case (legacy code bases with humongous unformatted areas): Auto-formatting only the affected part of the file that opencode had touched makes a lot of sense to me; formatting the rest of the file should be a different task / step anyways IMO.
Having such a system in place would not only be very beneficial for my workflow, I believe it would make opencode a better tool for everyone.
Feel feel to request changes or I'd also be happy to hand this branch over.
micuintus
@rekram1-node Is there any way I can increase your confidence with that topic?
micuintus
@rekram1-node Is there any way I can increase your confidence with that topic?
@rekram1-node bump :)
thdxr
this is good we're gonna merge it, aiden will follow up
rekram1-node
@micuintus can you update your code to follow some of our style guidelines a bit better? https://github.com/sst/opencode/blob/dev/STYLE_GUIDE.md
Ex: change changedRanges to just ranges, stuff like that
micuintus
@micuintus can you update your code to follow some of our style guidelines a bit better? https://github.com/sst/opencode/blob/dev/STYLE_GUIDE.md
Ex: change changedRanges to just ranges, stuff like that
@rekram1-node Sorry for the delay, I only just saw this. -> Done: 2d4778310978089f6001b303832cd29b377ebd12
Anything else?
rekram1-node
/review
rekram1-node
u can ignore that fail its whatever, ill merge tonight
micuintus
/review
rekram1-node
lol only i can trigger it
micuintus
@rekram1-node
lol only i can trigger it
LOL #FAIL :)
u can ignore that fail its whatever, ill merge tonight
Anything else I can do to help?
micuintus
Hm?
jasonfharris
Could this PR get fixed and merged?
micuintus
@rekram1-node ?
micuintus
@thdxr @rekram1-node: What's the status on this one now?
I'd prefer that we either merge this pull request or close it. I'd rather not leave it in limbo forever. 🥺 If more changes are required, I'm happy to work on them, of course.
micuintus
@thdxr @rekram1-node ? 🥺
rekram1-node
Oof this fell under radar again, u can always reach out via dm
rekram1-node
Ran a review with an llm to check over it, this could be bs:
Yes — here is a concrete, numeric proof using the code as written. Where the mismatch happens:
- calculateRanges diffs normalized content (\r\n → \n) and then accumulates charOffset and byteOffset from change.value lengths.
- Those offsets are later used as byte offsets for clang-format against the raw file content.
- That’s fine for LF, but wrong for CRLF because every \r\n is 2 bytes in the raw file and only 1 byte in the normalized diff. Proof by example (CRLF file, insert a line): Raw contents (CRLF): oldContent = "a\r\nb\r\nc\r\n" newContent = "a\r\nb\r\nX\r\nc\r\n" Normalized for diff (\r\n → \n): oldNorm = "a\nb\nc\n" newNorm = "a\nb\nX\nc\n" When the diff sees the added line "X\n", the offsets just before it are computed from newNorm:
- charOffset before "X\n" = length of "a\nb\n" = 4
- byteOffset before "X\n" = Buffer.byteLength("a\nb\n") = 4 So the range is produced with _byteOffset = 4. But in the raw newContent, the true byte offset before "X\r\n" is:
- "a\r\n" = 3 bytes
- "b\r\n" = 3 bytes
- total = 6 bytes So the correct byte offset is 6, not 4. The code undercounts by the number of preceding CRLFs (2 here). That means clang-format --offset=4 will start 2 bytes earlier than intended (inside the CRLF sequence or the previous line), and the formatted range is wrong or skipped. This is exactly the mismatch caused by:
- normalizing in calculateRanges (diffLines(oldContent.replace(/\r\n/g, "\n"), ...))
- but later using the computed _byteOffset against the raw file in buildRangeCommand for clang-format. That’s the bug. The fix is to compute offsets against the raw newContent (no CRLF normalization) or build a mapping from normalized indices back to raw indices when constructing DiffRange.
micuintus
@rekram1-node Thank you. It was not BS, valid points although not critical. Addressed it and straightened up the PR
micuintus
@rekram1-node Private DM... how would I send that? :)
micuintus
@rekram1-node Hey Aiden, Could you review again, please? Would be really nice to get this included.
micuintus
Review ran with GLM-5: <img width="942" height="489" alt="Screenshot 2026-02-19 at 12 05 57" src="https://github.com/user-attachments/assets/8d7b73c5-2aa5-4073-b683-607daca95736" />
micuintus
micuintus
@rekram1-node Merge? :)
micuintus
@thdxr
Since Aiden is out, can sb else from the team take that over? Who would that be?
It would help the development with legacy code bases I have to deal with a lot (also for my colleagues).
micuintus
@Hona @adamdotdevin anyone?
micuintus
@nexxeln Thanks! Should be fixed!
micuintus
Gemini 3.1 says:
1. CORRECT (100% sure?): Yes. The implementation correctly restricts formatting to edited lines. The logic for calculating ranges and mapping them to character/byte offsets is sound.
2. Minimal: Yes. The changes are surgically applied to the formatting service and tools, adding only necessary utilities and schema updates.
3. Project Standards: Yes. It follows the codebase's architectural patterns (Effect-TS, Zod, Bun) and maintains consistent styling.
4. Regressions:
* 4.a) Unicode surrogate pairs: Handled correctly. getByteOffset uses codePointAt and String.fromCodePoint to ensure non-BMP characters (like
emojis) are treated as single units, resulting in correct 4-byte UTF-8 offsets. I verified that NaN is not produced in the specific case of
calculateRanges("", "😀a\n").
* 4.b) CRLF problems: Handled correctly. Both getCharOffset and getByteOffset explicitly detect and account for \r\n sequences, ensuring
accurate line-to-offset mapping regardless of the platform's line endings.
Claude Opus 4.6 says:
Summary
Criterion Verdict
Correctness Pass — all logic is sound, edge cases handled
Minimality Pass — minor extras (DiffRange helpers, rlang fix) but justified
Coding standards Pass — follows project conventions
4a. Surrogate pairs No bug — offsets are correct, no NaN. Test exists.
4b. CRLF No bug — normalization + raw-offset functions handle it correctly. Tests exist.
The commit is clean. The only suggestion I'd add is a CRLF end-to-end test that goes through calculateRanges → getByteOffset for a CRLF file with emoji (combining both edge cases), but the individual tests already cover each dimension independently.
micuintus
Had to partially redo this due to some changes in the formatting architecture (effects), here are the new reviews:
<img width="1136" height="913" alt="Screenshot 2026-03-19 at 17 47 10" src="https://github.com/user-attachments/assets/115ea6c9-8293-478c-a1d2-2996cfabb14a" /> <img width="727" height="572" alt="Screenshot 2026-03-19 at 17 21 28" src="https://github.com/user-attachments/assets/20af7dba-56b4-4046-9502-bfd3ca27c040" />micuintus
Gemini Pro 3.1
1. Correctness: Yes, the changes are correct. The new implementation in packages/opencode/src/format/diff-range.ts correctly handles unicode surrogate pairs and CRLF line endings.
2. Minimality: Yes, the changes are minimal and focused on extracting range calculation logic into a reusable module.
3. Standards: Yes, the code follows project coding standards (TypeScript, proper types, tests).
4. Regressions: I have verified the reported regression:
- Unicode Surrogate Pairs: The implementation uses a tokenizer that correctly identifies surrogate pairs (e.g., tokenize("😀") returns a single token). calculateRanges("", "😀a\n") produces valid byte
offsets, and byteEnd is not NaN.
- CRLF: The implementation handles CRLF by tokenizing \r\n as a single unit, ensuring alignment with normalized diffs while correctly counting bytes (2 bytes for \r\n).
- Tests: A specific regression test test("unicode surrogate pairs", ...) is included in packages/opencode/test/diff-range.test.ts.
micuintus
Claude Opus 4.6 review:
┌─────┬───────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────┐
│ # │ Check │ Result │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 1 │ Correctness │ Pass │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 2 │ Minimality │ Pass │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 3 │ Coding standards │ Pass │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 4a │ Surrogate pair │ Pass — tokenize regex handles them correctly, byteEnd=NaN cannot occur. Tests at lines │
│ │ regression │ 79-113. │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 4b │ CRLF regression │ Pass — normalization preserves token count alignment. Tests at lines 65-135. │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 5 │ All tools covered │ Pass — write, edit, apply_patch all pass ranges. No emitters missed. │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 6 │ Range merging │ Pass — collapse() merges adjacent ranges; clang batches natively, prettier/ruff │
│ │ │ reverse-sort to avoid offset shifts. │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 7 │ Architecture │ Pass — backward-compatible via optional schema/interface, clean event-driven │
│ │ │ integration. │
├─────┼───────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ 8 │ Formatter coverage │ Pass — prettier, clang-format, ruff have range support; 22 others fall back safely to │
│ │ │ full-file formatting. │
└─────┴───────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────┘
46 tests pass. No changes required.
micuintus
Gemini 3.1 Pro:
Summary of Review
After a comprehensive audit of the changes against origin/dev, I can confirm the following:
* Correctness (10000% Sure): The logic is sound. The specific concern regarding byteEnd=NaN for surrogate pairs is
non-existent in the current implementation; the code explicitly uses TextEncoder on individual tokens to ensure accurate
byte calculations.
* Minimal & Clean: The changes are surgical. They introduce the calculateRanges utility and hook it into the Format.Service
without bloating the existing tool logic.
* Coding Standards: The implementation strictly adheres to the project's architectural patterns (using Effect, Bus events,
and Zod for schema validation).
* Regression Proof:
* Unicode: Non-BMP characters are handled correctly via the surrogate-aware tokenizer.
* CRLF: Line-ending normalization ensures that line-ending-only changes do not trigger ranges.
* Tool Integration: The feature is globally available to any tool that publishes the File.Event.Edited event. It is
currently implemented for write, edit, and apply_patch.
* Multi-Range Strategy:
* Merging: Tools that support it (like clang-format) have their ranges merged into a single process call.
* Isolation: Tools that don't support it use a "reverse-sorted" execution strategy, ensuring that formatting a range at
the end of a file doesn't break the character offsets of ranges earlier in the file.
* Architectural Fit: This is a "pristine" feature. It leverages the existing decoupled Bus architecture, allowing
formatters to react to file edits without the tools needing to know the specifics of each formatter's CLI arguments.
Verdict: The PR is high-quality, handles the identified edge cases correctly, and is ready for merge.
micuintus
Note: I had to add two unrelated commits to fix issues with the pipeline on origin/dev
Changed Files
packages/app/src/components/dialog-connect-provider.tsx
+1−1packages/app/src/components/dialog-custom-provider.tsx
+1−1packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+1−1packages/opencode/src/file/index.ts
+14−0packages/opencode/src/format/diff-range.ts
+179−0packages/opencode/src/format/formatter.ts
+35−2packages/opencode/src/format/index.ts
+26−19packages/opencode/src/tool/apply_patch.ts
+3−0packages/opencode/src/tool/edit.ts
+5−0packages/opencode/src/tool/write.ts
+3−0packages/opencode/test/diff-range.test.ts
+252−0packages/opencode/test/format/format.test.ts
+52−0