Prerequisites
- I have searched for duplicate or closed issues
- I have read the contributing guidelines
Describe the issue
Hi! We're using the diff editor in an Electron app, and we keep losing the word diff marks (data-diff-span) on the new side while the editor is attached. The old side always keeps them, the syntax colors stay, and ending the edit session brings them back.
You can see it on https://diffs.com/edit: click "diff", then "split". The new side loses its marks, and switching back to unified doesn't bring them back.
We tried a lot of combinations, and it comes down to these triggers:
- Highlights, attaching the editor. The marks go right away with a
Virtualizer, or at the next re-render without one (adiffStyleoroverflowchange, for example). That's the diffs.com case. - Shiki or Highlights, a theme change while editing. Either the page's color scheme flips (
themeType: 'system') orthemeis changed (with a single theme name this one didn't happen every time).setThemeType()doesn't trigger it.
With no editor attached, nothing is lost in any of the cases we tried.
I think it's one cause. In both cases the editor's tokenizer runs a background pass over the whole document (after attach for Highlights, after setTheme for both), and each batch goes through onDeferTokenize to FileDiff.updateRenderCache. That rebuilds the new side's cached rows from the editor's tokens, which have no data-diff-span. Later renders reuse that cache, so the marks stay gone. With a Virtualizer, the editor also repaints the visible rows straight away, which is why it shows at once there.
Expected: the word marks stay while the editor is attached, on both sides.
Reproduction
<style>body { color-scheme: dark } body.light { color-scheme: light }</style>
<div id="scroller" style="height: 2000px; overflow: auto"><div id="diff"></div></div>import { Virtualizer, VirtualizedFileDiff } from '@pierre/diffs';
import { Editor } from '@pierre/diffs/edit';
const PREFERRED_HIGHLIGHTER = 'highlights'; // or 'shiki-js'
const code = (changed: boolean) =>
Array.from({ length: 30 }, (_, i) => `export function f${i}(x: number): number {\n return ${changed && i === 15 ? 'y' : 'x'} + ${i};\n}`).join('\n') +
`\nconst s = 'foo again${changed ? '170' : ''}';\n`;
const host = document.getElementById('diff')!;
const virtualizer = new Virtualizer();
virtualizer.setup(document.getElementById('scroller')!);
const view = new VirtualizedFileDiff(
{ theme: { dark: 'pierre-dark', light: 'pierre-light' }, preferredHighlighter: PREFERRED_HIGHLIGHTER, lineDiffType: 'word' },
virtualizer,
);
view.render({ fileContainer: host, oldFile: { name: 'example.ts', contents: code(false) }, newFile: { name: 'example.ts', contents: code(true) } });
const marks = () => {
const rows = [...host.shadowRoot!.querySelectorAll('[data-diff-span]')].map((mark) => mark.closest('[data-line-type]')?.getAttribute('data-line-type'));
return `old side ${rows.filter((t) => t === 'change-deletion').length}, new side ${rows.filter((t) => t === 'change-addition').length}`;
};
const settle = () => new Promise((resolve) => setTimeout(resolve, 1000));
await settle();
console.log('1. rendered, read-only:', marks());
const editor = new Editor('file-diff');
const endEditing = editor.edit(view);
await settle();
console.log('2. editor attached:', marks());
document.body.classList.add('light'); // the page's color scheme turns light
await settle();
console.log('3. color scheme flipped while editing:', marks());
endEditing();
await settle();
console.log('4. editing ended:', marks());What it logs:
highlights:
1. rendered, read-only: old side 2, new side 2
2. editor attached: old side 2, new side 0
3. color scheme flipped while editing: old side 2, new side 0
4. editing ended: old side 2, new side 2
shiki-js:
1. rendered, read-only: old side 2, new side 2
2. editor attached: old side 2, new side 2
3. color scheme flipped while editing: old side 2, new side 0
4. editing ended: old side 2, new side 2To load themes at all, I had to patch @pierre/theming locally (#1207).
Tested in headless Chromium 156, with @pierre/diffs 1.6.0, @pierre/highlights 0.1.3 and shiki 4.5.0. The theme change case also happens on 1.5.1. We haven't tried the React components, CodeView, worker pools, or Firefox and Safari.
What browser(s) are you seeing the problem on?
Chrome
What version of @pierre/diffs are you using?
1.6.0
Activity
5Updated the title and description with everything we found. It's not only Highlights: with Shiki a theme change while editing does the same, and you can see it on https://diffs.com/edit by switching to split. Same repro covers both.
Oki, so digging into this a bit, there are def some changes that came from the 1.6 update that make this more noticeable in some cases, but some context -- we intentionally do not want to actually run the word diff highlighting while editing, and not until you save or commit the edit changes. I think in the past we typically just let the markers stay until you edit a line.
Part of the rationale was that editing uses a simpler/faster diffing system to align stuff that may change when you actually save or commit the changes. Thus deferring the line diffs until then.
My preference to fix this is leaning on being more explicit: when going into edit mode, we just disable/hide all line diffs immediately so things don't feel confusing or happen in weird/inconsistent ways.
What do you think?
@amadeus thanks for digging in and for explaining the reasoning.
In our case, the right side of the diff is always editable when the right file is on disk. So for us there's no edit mode to go into, the editor is attached as soon as the diff shows, so people can just click and type. Hiding line diffs in edit mode would mean our diffs never show word marks at all, even when nobody types.
We also think keeping them live while editing: you type and see right away what you changed, word marks included. We already do this on our side on top of the editor. After a short typing pause (300 ms) we redo the word diffs for the changed line pairs from the session's diff, and rebuild only the rows whose marks changed, from the tokens they already have. It's cheap: the same word diff you use, a few microseconds per line pair, capped at 1,000 pairs. Since the marks come from the session's own alignment, they always match what's shown as changed. If the alignment changes on save, the marks just move with it. The UX is quite good that way.
So could "live line diffs be an option"? It would also let us drop the code we patch in today. Happy to help sharing our approach if that's useful.
word, heard, i'll look into it, no promises tho!
Folded into the description above, along with the other cases we found.