Skip to content
Draft
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
96 changes: 85 additions & 11 deletions packages/diffs/src/components/UnresolvedFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ interface ResolveConflictReturn {
markerRows: MergeConflictMarkerRow[];
}

interface PendingControlledRender<LAnnotation> {
fileDiff: FileDiffMetadata;
actions: (MergeConflictDiffAction | undefined)[];
markerRows: MergeConflictMarkerRow[];
lineAnnotations: UnresolvedFileRenderProps<LAnnotation>['lineAnnotations'];
}

interface ActiveMergeConflictState {
actions: (MergeConflictDiffAction | undefined)[];
markerRows: MergeConflictMarkerRow[];
fileDiff: FileDiffMetadata | undefined;
}

type UnresolvedFileDataCache = GetOrComputeDiffProps;

let instanceId = -1;
Expand All @@ -124,6 +137,9 @@ export class UnresolvedFile<
private markerRows: MergeConflictMarkerRow[] = [];
private conflictActionCache: Map<string, MergeConflictActionElementCache> =
new Map();
private pendingControlledRender:
| PendingControlledRender<LAnnotation>
| undefined;

constructor(
public override options: UnresolvedFileOptions<LAnnotation> = {
Expand Down Expand Up @@ -205,6 +221,7 @@ export class UnresolvedFile<
markerRows: undefined,
};
this.conflictActions = [];
this.pendingControlledRender = undefined;
super.cleanUp();
}

Expand Down Expand Up @@ -354,7 +371,11 @@ export class UnresolvedFile<
return;
}
this.hydrateElements(fileContainer, prerenderedHTML);
this.setActiveMergeConflictState(source.actions, source.markerRows);
this.setActiveMergeConflictState({
actions: source.actions,
markerRows: source.markerRows,
fileDiff: source.fileDiff,
});
// If we have no pre tag, then we should render
if (this.pre == null) {
this.render({ ...props, preventEmit: true });
Expand All @@ -371,7 +392,25 @@ export class UnresolvedFile<
}

override rerender(): void {
if (!this.enabled || this.fileDiff == null) {
if (!this.enabled) {
return;
}
const pending = this.pendingControlledRender;
if (pending != null) {
if (!this.hunksRenderer.canRenderDiffSynchronously(pending.fileDiff)) {
return;
Comment on lines +400 to +401
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Requeue pending controlled render when sync check fails

When a controlled unresolved update is pending, this branch returns immediately if canRenderDiffSynchronously(...) is false, but it does not call prepareDiffRender(...) to enqueue work for the current render options. That can deadlock the pending state after worker option changes: WorkerPoolManager.setRenderOptions() clears diff cache and triggers rerender(), this method exits here, and no new highlight task is scheduled for the pending diff, so the merge-conflict UI can stay stuck on the old state indefinitely until some unrelated render(...) call happens.

Useful? React with 👍 / 👎.

}
this.render({
fileDiff: pending.fileDiff,
actions: pending.actions,
markerRows: pending.markerRows,
lineAnnotations: pending.lineAnnotations,
forceRender: true,
renderRange: this.renderRange,
});
return;
}
if (this.fileDiff == null) {
return;
}
this.render({ forceRender: true, renderRange: this.renderRange });
Expand All @@ -396,7 +435,26 @@ export class UnresolvedFile<
if (source == null) {
return false;
}
this.setActiveMergeConflictState(source.actions, source.markerRows);
if (
this.shouldDeferControlledRender(source.fileDiff) &&
!this.hunksRenderer.canRenderDiffSynchronously(source.fileDiff)
) {
this.pendingControlledRender = {
fileDiff: source.fileDiff,
actions: source.actions,
markerRows: source.markerRows,
lineAnnotations,
};
this.hunksRenderer.prepareDiffRender(source.fileDiff);
return false;
}

this.pendingControlledRender = undefined;
this.setActiveMergeConflictState({
actions: source.actions,
markerRows: source.markerRows,
fileDiff: source.fileDiff,
});
const didRender = super.render({
...rest,
fileDiff: source.fileDiff,
Expand Down Expand Up @@ -477,7 +535,7 @@ export class UnresolvedFile<
}

this.computedCache = { file, fileDiff, actions, markerRows };
this.setActiveMergeConflictState(actions, markerRows);
this.setActiveMergeConflictState({ actions, markerRows, fileDiff });
// NOTE(amadeus): This is a bit jank, but helps to ensure we don't see a
// bunch of jittery re-renders as things resolve out. In a more perfect
// world we would have a more elegant way to kick off a render to the
Expand All @@ -494,20 +552,30 @@ export class UnresolvedFile<
this.options.onMergeConflictResolve?.(file, payload);
}

private setActiveMergeConflictState(
actions: (MergeConflictDiffAction | undefined)[] = this.conflictActions,
markerRows: MergeConflictMarkerRow[] = this.markerRows
): void {
private shouldDeferControlledRender(fileDiff: FileDiffMetadata): boolean {
return (
this.options.onMergeConflictAction != null &&
this.workerManager?.isWorkingPool() === true &&
this.fileDiff != null &&
fileDiff !== this.fileDiff
);
}

private setActiveMergeConflictState({
actions = this.conflictActions,
markerRows = this.markerRows,
fileDiff = this.fileDiff,
}: ActiveMergeConflictState): void {
this.conflictActions = actions;
this.markerRows = markerRows;
if (
this.computedCache.fileDiff != null &&
fileDiff != null &&
this.hunksRenderer instanceof UnresolvedFileHunksRenderer
) {
this.hunksRenderer.setConflictState(
this.options.mergeConflictActionsType === 'none' ? [] : actions,
markerRows,
this.computedCache.fileDiff
fileDiff
);
}
}
Expand All @@ -525,6 +593,12 @@ export class UnresolvedFile<
"UnresolvedFile.handleMergeConflictActionClick: conflictIndex and conflictAction don't match"
);
}
if (
this.pendingControlledRender != null &&
this.options.onMergeConflictAction != null
) {
return;
}
const payload: MergeConflictActionPayload = {
resolution: target.resolution,
conflict: action.conflict,
Expand All @@ -537,7 +611,7 @@ export class UnresolvedFile<
};

private renderMergeConflictActionSlots(): void {
const { fileDiff } = this.computedCache;
const fileDiff = this.fileDiff;
if (
this.isContainerManaged ||
this.fileContainer == null ||
Expand Down
69 changes: 63 additions & 6 deletions packages/diffs/src/renderers/DiffHunksRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,14 +392,11 @@ export class DiffHunksRenderer<LAnnotation = undefined> {
}
this.diff = diff;
const { options } = this.getRenderOptions(diff);
let cache = this.workerManager?.getDiffResultCache(diff);
if (cache != null && !areDiffRenderOptionsEqual(options, cache.options)) {
cache = undefined;
}
const cache = this.syncRenderCacheFromWorkerCache(diff, options);
this.renderCache ??= {
diff,
highlighted: !isDiffPlainText(diff),
options,
options: cache?.options ?? options,
result: cache?.result,
renderRange: undefined,
};
Expand Down Expand Up @@ -445,6 +442,64 @@ export class DiffHunksRenderer<LAnnotation = undefined> {
return { options, forceRender: false };
}

private getWorkerDiffResultCache(
diff: FileDiffMetadata,
options: RenderDiffOptions
): RenderDiffResult | undefined {
const cache = this.workerManager?.getDiffResultCache(diff);
if (cache == null || !areDiffRenderOptionsEqual(options, cache.options)) {
return undefined;
}
return cache;
}

private syncRenderCacheFromWorkerCache(
diff: FileDiffMetadata,
options: RenderDiffOptions
): RenderDiffResult | undefined {
const cache = this.getWorkerDiffResultCache(diff, options);
if (
cache != null &&
(this.renderCache == null ||
this.renderCache.diff !== diff ||
!this.renderCache.highlighted ||
!areDiffRenderOptionsEqual(this.renderCache.options, cache.options))
) {
this.renderCache = {
diff,
options: cache.options,
highlighted: true,
result: cache.result,
renderRange: undefined,
};
}
return cache;
}

public canRenderDiffSynchronously(diff: FileDiffMetadata): boolean {
if (this.workerManager?.isWorkingPool() !== true || isDiffPlainText(diff)) {
return true;
}
const { options } = this.getRenderOptions(diff);
this.syncRenderCacheFromWorkerCache(diff, options);
return (
this.renderCache?.result != null &&
this.renderCache.highlighted &&
this.renderCache.diff === diff &&
areDiffRenderOptionsEqual(this.renderCache.options, options)
);
}

public prepareDiffRender(diff: FileDiffMetadata): void {
if (this.workerManager?.isWorkingPool() !== true || isDiffPlainText(diff)) {
return;
}
if (this.canRenderDiffSynchronously(diff)) {
return;
}
this.workerManager.highlightDiffAST(this, diff);
}

public renderDiff(
diff: FileDiffMetadata | undefined = this.renderCache?.diff,
renderRange: RenderRange = DEFAULT_RENDER_RANGE
Expand All @@ -463,7 +518,9 @@ export class DiffHunksRenderer<LAnnotation = undefined> {
...cache,
};
}
const { options, forceRender } = this.getRenderOptions(diff);
let { options, forceRender } = this.getRenderOptions(diff);
this.syncRenderCacheFromWorkerCache(diff, options);
({ options, forceRender } = this.getRenderOptions(diff));
this.renderCache ??= {
diff,
highlighted: false,
Expand Down
Loading