Skip to content

fix(diff): preserve tab indentation in diff view#606

Merged
idursun merged 2 commits into
idursun:mainfrom
baggiiiie:yc/fix/tab
Mar 20, 2026
Merged

fix(diff): preserve tab indentation in diff view#606
idursun merged 2 commits into
idursun:mainfrom
baggiiiie:yc/fix/tab

Conversation

@baggiiiie

Copy link
Copy Markdown
Collaborator

Bug:
Diff view dropped indentation on lines containing tabs, while preview mode rendered the same diff correctly.

Root cause:
The diff pane rendered lines directly with Ultraviolet, which does not expand tab characters. It also computed wrap and horizontal-scroll widths from the unexpanded text, so tabs were wrong for both drawing and layout.

Fix:
Expand tabs when diff content is loaded, before storing lines and computing widths. Use the same Lip Gloss tab expansion behavior as preview so rendering, wrapping, and horizontal scrolling all stay consistent. Add regression tests for rendering, wrapping, and horizontal scrolling with tab-indented lines.

Closes #604

issue before fix:

Preview works fine:
image

Diff view is breaking:
image

@baggiiiie baggiiiie requested a review from idursun as a code owner March 18, 2026 18:05

@idursun idursun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks!

There's a comment I think we should address.

Comment thread internal/ui/render/tabs.go Outdated
@baggiiiie

Copy link
Copy Markdown
Collaborator Author

@idursun i pushed another commit in order to deal with tabs not starting at column 0

the solution is getting longer, but it matches the default output from jj diff now

image

@baggiiiie baggiiiie requested a review from idursun March 19, 2026 07:31

@idursun idursun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey thanks!

There are one comment about multi line treatment. One additional comment about potential deadcode.

Comment thread internal/ui/render/tabs.go Outdated
return s
}

lines := strings.Split(s, "\n")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are already splitting by line before calling this function but then doing the same thing here. I'd expect ExpandTabs to deal with a single line and leave the splitting to the caller.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, though preview doesn't originally split lines

func (m *Model) SetContent(content string) {
content = strings.ReplaceAll(content, "\r", "")
m.reset()
m.content = content
m.view.SetContent(content)
}

but diff does:

content = "(empty)"
}
lines := strings.Split(content, "\n")
maxWidth := 0
for _, line := range lines {

Comment thread internal/ui/render/tabs.go Outdated
@baggiiiie

Copy link
Copy Markdown
Collaborator Author

@idursun thanks for the detailed review!

i added line splitting for preview to keep the expandTab function a bit more lightweight.
as #575 "drop viewport and use render package instead", there's some discrepency between how these two models deal with rendering.

maybe we do decide to port preview to the same custom renderer? it seems nice to also have wrapping and width aware layout for preview.

for now, im keeping this PR simple and didn't touch that logic

Bug:
Diff view dropped indentation on lines containing tabs, while preview mode rendered the same diff correctly.

Root cause:
The diff pane rendered lines directly with Ultraviolet, which does not expand tab characters. It also computed wrap and horizontal-scroll widths from the unexpanded text, so tabs were wrong for both drawing and layout.

Fix:
Expand tabs when diff content is loaded, before storing lines and computing widths. Use the same Lip Gloss tab expansion behavior as preview so rendering, wrapping, and horizontal scrolling all stay consistent. Add regression tests for rendering, wrapping, and horizontal scrolling with tab-indented lines.

@idursun idursun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@idursun idursun merged commit acb5701 into idursun:main Mar 20, 2026
4 checks passed
@baggiiiie baggiiiie deleted the yc/fix/tab branch March 21, 2026 08:25
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Apr 21, 2026
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [idursun/jjui](https://github.com/idursun/jjui) | patch | `v0.10.2` → `v0.10.3` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>idursun/jjui (idursun/jjui)</summary>

### [`v0.10.3`](https://github.com/idursun/jjui/releases/tag/v0.10.3)

[Compare Source](idursun/jjui@v0.10.2...v0.10.3)

This release includes new Lua customisation support, repo-local configuration, preview sizing improvements, and a set of UI fixes. There were also some internal changes around action routing and rendering, so if something feels broken or behaves differently, please let me know.

#### Features

##### Repository-local configuration

Repository-local configuration is now supported. You can add `.jjui/config.toml` or `.jjui/config.lua` to a repository to customise jjui for that repo while still inheriting global configuration. This is useful for repo-specific revsets, limits, bindings, and Lua configuration. `JJUI_CONFIG_DIR` remains a hard override when set. [#&#8203;609](idursun/jjui#609)

##### Lua editor metadata

Lua scripting now has installable editor metadata. Run `jjui --install-lua-types` to write generated `types.lua` metadata to your jjui config directory and create `.luarc.json` for LuaLS when one does not already exist. This gives Lua users autocomplete and basic type information for `config.lua`, plugins, and embedded Lua scripts. [#&#8203;617](idursun/jjui#617)

##### Preview pane sizing

Preview commands now receive the preview pane size through `COLUMNS` and `LINES`. Width-sensitive tools such as `difft`, and configured `delta` setups, can now wrap output to the jjui preview pane instead of the outer terminal width. [#&#8203;608](idursun/jjui#608)

##### Layered revision operations

Revision operations can now be layered more naturally, with active operation scopes reflected in key handling and help/status display. Active key bindings are grouped by their current scope, and overridden bindings are shown with strikethrough, making it easier to understand which shortcuts are available in nested operations. [#&#8203;625](idursun/jjui#625)

<img width="1508" height="397" alt="Scoped status/help display" src="https://github.com/user-attachments/assets/c3d1ec20-10db-4213-a683-69c9b162aec0" />

#### Improvements
##### Inline describe customisation

Inline describe is now easier to customise from bindings and Lua. You can bind `revisions.inline_describe.new_line` separately from `revisions.inline_describe.accept`, and `accept` can take `force = true` for workflows that intentionally update immutable revisions. [#&#8203;260](idursun/jjui#260)

```toml
[[bindings]]
action = "revisions.inline_describe.accept"
key = "enter"
scope = "revisions.inline_describe"

[[bindings]]
action = "revisions.inline_describe.new_line"
key = "shift+enter"
scope = "revisions.inline_describe"
```

- jjui now refreshes only once on startup instead of doing duplicate initial refresh work.
- Git operation screens now close when the underlying command starts executing, making the transition from choosing an action to running it clearer.
- Debug logging now uses `JJUI_DEBUG` instead of the generic `DEBUG` environment variable, reducing the chance that unrelated shell configuration accidentally enables jjui debug behaviour.

#### Fixes

- Fixed paste handling in Bubble Tea v2 text-input flows, including pasting into the bookmark filter input. [#&#8203;601](idursun/jjui#601), [#&#8203;597](idursun/jjui#597)
- Fixed rebase source plus insert-between so jjui generates the correct `jj rebase -s <source> --insert-before <rev> --insert-after <rev>` command. [#&#8203;598](idursun/jjui#598)
- Fixed diff rendering for tab-indented lines. The diff view now preserves indentation and handles wrapping and horizontal scrolling correctly for tab-expanded content. [#&#8203;606](idursun/jjui#606), [#&#8203;604](idursun/jjui#604)
- Fixed command history rendering so long histories are not clipped at the top border, including exact-fit cases. Flash and command-history overlays also layer more cleanly above the status area. [#&#8203;626](idursun/jjui#626), [#&#8203;622](idursun/jjui#622)
- Fixed fuzzy file overlay ordering so the fuzzy file UI appears in the correct layer.
- Fixed clipped rendering for partially visible list items and fallback messages while horizontally scrolled.
- Fixed revset alias parameter metadata preservation.

#### What's Changed

- Handle Bubble Tea paste messages in text-input flows by [@&#8203;shardulbee](https://github.com/shardulbee) in [#&#8203;601](idursun/jjui#601)
- Set preview subprocess size via env by [@&#8203;OliverJAsh](https://github.com/OliverJAsh) in [#&#8203;608](idursun/jjui#608)
- add repo config file which overrides global config.toml by [@&#8203;nickchomey](https://github.com/nickchomey) in [#&#8203;609](idursun/jjui#609)
- fix(diff): preserve tab indentation in diff view by [@&#8203;baggiiiie](https://github.com/baggiiiie) in [#&#8203;606](idursun/jjui#606)
- Update installation instruction for archlinux by [@&#8203;TeddyHuang-00](https://github.com/TeddyHuang-00) in [#&#8203;613](idursun/jjui#613)
- refactor: replace owner-based dispatch with scope-layer routing by [@&#8203;idursun](https://github.com/idursun) in [#&#8203;625](idursun/jjui#625)
- fix: command history clipping by [@&#8203;idursun](https://github.com/idursun) in [#&#8203;626](idursun/jjui#626)
- fix(set\_parents): leak set\_parents operation key scope by [@&#8203;baggiiiie](https://github.com/baggiiiie) in [#&#8203;628](idursun/jjui#628)
- fix(details): sync details selection for routed intents by [@&#8203;baggiiiie](https://github.com/baggiiiie) in [#&#8203;627](idursun/jjui#627)
- fix: add back esc to clear selected revisions by [@&#8203;baggiiiie](https://github.com/baggiiiie) in [#&#8203;629](idursun/jjui#629)
- fix(revisions): avoid toggle-select panic on empty revisions by [@&#8203;baggiiiie](https://github.com/baggiiiie) in [#&#8203;636](idursun/jjui#636)
- feat(lua): install Lua meta types by [@&#8203;idursun](https://github.com/idursun) in [#&#8203;617](idursun/jjui#617)

#### New Contributors

- [@&#8203;shardulbee](https://github.com/shardulbee) made their first contribution in [#&#8203;601](idursun/jjui#601)
- [@&#8203;OliverJAsh](https://github.com/OliverJAsh) made their first contribution in [#&#8203;608](idursun/jjui#608)

**Full Changelog**: <idursun/jjui@v0.10.2...v0.10.3>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever MR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjcuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEyNy4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6cGF0Y2giXX0=-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lines indented with tabs are rendered unindented in diff view

2 participants