Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MonacoModelRegistry } from './monaco-model-registry';
const rpcState = vi.hoisted(() => ({
indexContent: 'base' as string | null,
refContent: 'base' as string | null,
diskContent: 'base' as string,
}));

vi.mock('@renderer/lib/ipc', () => ({
Expand All @@ -13,7 +14,7 @@ vi.mock('@renderer/lib/ipc', () => ({
fs: {
readFile: vi.fn(async () => ({
success: true,
data: { content: 'base', truncated: false, totalSize: 4 },
data: { content: rpcState.diskContent, truncated: false, totalSize: 4 },
})),
},
git: {
Expand Down Expand Up @@ -105,6 +106,89 @@ describe('MonacoModelRegistry', () => {
beforeEach(() => {
rpcState.indexContent = 'base';
rpcState.refContent = 'base';
rpcState.diskContent = 'base';
});

it('bumps bufferVersions when a disk reload updates a clean open buffer', async () => {
// Regression: a disk-driven reload (e.g. after discard) updates the buffer
// model's content, but the onDidChangeContent listener bails while
// reloadingFromDisk is set and never bumps bufferVersions. Read-only reactive
// consumers (the rendered markdown/html/svg preview) subscribe to
// bufferVersions, so without an explicit bump they stay stale until remount.
const registry = new MonacoModelRegistry();
registry.notifyMonacoReady(makeFakeMonaco() as never);

const projectId = 'project';
const workspaceId = 'workspace';
const root = `workspace:${workspaceId}`;
const filePath = 'README.md';
const language = 'markdown';

rpcState.diskContent = 'base';
const uri = await registry.registerModel(
projectId,
workspaceId,
root,
filePath,
language,
'disk'
);
await registry.registerModel(projectId, workspaceId, root, filePath, language, 'buffer');
const diskUri = registry.toDiskUri(uri);

expect(registry.getValue(uri)).toBe('base');
const versionBefore = registry.bufferVersions.get(uri) ?? 0;

// Simulate discard / external revert: disk now holds the reverted content.
rpcState.diskContent = 'reverted';
await registry.invalidateModel(diskUri);

expect(registry.getValue(uri)).toBe('reverted');
expect(registry.bufferVersions.get(uri) ?? 0).toBeGreaterThan(versionBefore);
});

it('bumps bufferVersions when accepting incoming disk content for a conflicted buffer', async () => {
// Same regression as above, conflict-dialog path: reloadFromDisk ("Accept
// Incoming") sets the buffer from disk while reloadingFromDisk suppresses
// the onDidChangeContent listener, so it must bump bufferVersions itself.
const registry = new MonacoModelRegistry();
registry.notifyMonacoReady(makeFakeMonaco() as never);

const projectId = 'project';
const workspaceId = 'workspace';
const root = `workspace:${workspaceId}`;
const filePath = 'README.md';
const language = 'markdown';

rpcState.diskContent = 'base';
const uri = await registry.registerModel(
projectId,
workspaceId,
root,
filePath,
language,
'disk'
);
await registry.registerModel(projectId, workspaceId, root, filePath, language, 'buffer');
const diskUri = registry.toDiskUri(uri);

// User edits the buffer, then the file changes on disk underneath them:
// applyDiskUpdate must record a conflict instead of clobbering the edit.
registry.getModelByUri(uri)?.setValue('user edit');
expect(registry.isDirty(uri)).toBe(true);
rpcState.diskContent = 'external change';
await registry.invalidateModel(diskUri);

expect(registry.hasPendingConflict(uri)).toBe(true);
expect(registry.getValue(uri)).toBe('user edit');
const versionBefore = registry.bufferVersions.get(uri) ?? 0;

registry.reloadFromDisk(uri);

expect(registry.getValue(uri)).toBe('external change');
expect(registry.isDirty(uri)).toBe(false);
expect(registry.hasPendingConflict(uri)).toBe(false);
expect(registry.bufferVersions.get(uri) ?? 0).toBeGreaterThan(versionBefore);
});

it('clears a staged git model when the index no longer contains the file', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,10 @@ export class MonacoModelRegistry {
this.reloadingFromDisk.delete(uri);
runInAction(() => {
this.dirtyUris.delete(uri);
// setValue fires onDidChangeContent, but it bails while reloadingFromDisk is
// set and never bumps bufferVersions — bump it here so preview renderers
// re-render (same reasoning as applyDiskUpdate).
this.bufferVersions.set(uri, (this.bufferVersions.get(uri) ?? 0) + 1);
});
}
this.pendingConflicts.delete(uri);
Expand Down Expand Up @@ -840,6 +844,14 @@ export class MonacoModelRegistry {
const fullRange = bufEntry.model.getFullModelRange();
bufEntry.model.applyEdits([{ range: fullRange, text: newContent }], false);
this.reloadingFromDisk.delete(bufferUri);
// applyEdits fires onDidChangeContent, but that listener bails out while
// reloadingFromDisk is set, so it never bumps bufferVersions. Bump it here
// so observer() renderers that read buffer text (the markdown/html/svg
// preview) re-render with the reloaded content instead of staying stale
// until the tab is remounted.
runInAction(() => {
this.bufferVersions.set(bufferUri, (this.bufferVersions.get(bufferUri) ?? 0) + 1);
});
}
// Clear dirty state — disk now matches buffer (either buffer was synced to disk, or
// new disk content already matched existing buffer edits).
Expand Down