Skip to content

Commit 2f05fc3

Browse files
committed
Fix quick-fix lifecycle: session provenance, aria suppression, and dedup simplification
- Add session-provenance stamping to the output cache so outputs produced this session keep Fix/Explain buttons even after a save-as (file->file or untitled->file) reloads them from the cache. Previous behavior unconditionally marked all cache-restored outputs as stale. - Suppress screen-reader announcements for Fix/Explain button insertions by adding aria-live='off' to the quick-fix container (buttons mount asynchronously inside the output's role='log' live region). - Simplify the duplicate-mount guard from a replace-then-dispose dance with a separate _quickFixContainersByKey map to a first-wins has() skip (the extra map is deleted). - Resolve getCellContext at click time via a reverse view-zone lookup instead of capturing cellId in a closure that must be manually rebound at every remap site. - Re-key the stash on untitled->file save-as so outputs restore from the stash (preserving live flags) instead of re-reading from the just- transferred cache. - Remove the redundant in-memory seeding in _transferCacheFromUntitled that caused doubled outputs after a stash round trip.
1 parent de2b063 commit 2f05fc3

5 files changed

Lines changed: 235 additions & 42 deletions

File tree

src/vs/workbench/contrib/positronQuarto/browser/quartoOutputCacheService.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ interface IpynbCell {
4343
quarto_cell_id: string;
4444
quarto_content_hash: string;
4545
quarto_label?: string;
46+
quarto_session_id?: string;
4647
};
4748
source: string[];
4849
outputs: IpynbOutput[];
@@ -104,6 +105,8 @@ interface CellCacheEntry {
104105
label?: string;
105106
source?: string;
106107
outputs: ICellOutput[];
108+
/** Session that last wrote this cell's outputs; see _sessionId. */
109+
sessionId?: string;
107110
}
108111

109112
/**
@@ -114,6 +117,15 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
114117
declare readonly _serviceBrand: undefined;
115118

116119
private readonly _cacheDir: URI;
120+
121+
/**
122+
* Identifies this window session in cache entries. Cells written via
123+
* saveOutput are stamped with it (in memory and on disk), so restores can
124+
* tell outputs produced this session (e.g. re-read after a save-as) from
125+
* outputs that survive from a previous session.
126+
*/
127+
private readonly _sessionId = generateUuid();
128+
117129
private readonly _documentCaches = new Map<string, DocumentCacheEntry>();
118130
private readonly _dirtyDocuments = new Set<string>();
119131
private readonly _pendingWrites = new Map<string, Promise<void>>();
@@ -280,6 +292,8 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
280292
label: cell.label,
281293
source: cell.source,
282294
outputs: [...cell.outputs],
295+
// Preserve session provenance across the disk round trip.
296+
sessionId: cell.fromCurrentSession ? this._sessionId : undefined,
283297
});
284298
}
285299

@@ -345,6 +359,10 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
345359
cellEntry.source = source;
346360
}
347361

362+
// Stamp the cell with the current session so restores can tell these
363+
// outputs from ones cached in a previous session.
364+
cellEntry.sessionId = this._sessionId;
365+
348366
// Filter out the data explorer MIME type before caching. The data
349367
// explorer requires a live runtime connection that won't exist when
350368
// restoring from cache; text/plain is kept as the fallback.
@@ -674,6 +692,8 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
674692
label: cell.label,
675693
source: cell.source,
676694
outputs: [...cell.outputs],
695+
// Preserve session provenance across the disk round trip.
696+
sessionId: cell.fromCurrentSession ? this._sessionId : undefined,
677697
});
678698
}
679699

@@ -775,6 +795,7 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
775795
quarto_cell_id: cellEntry.cellId,
776796
quarto_content_hash: cellEntry.contentHash,
777797
quarto_label: cellEntry.label,
798+
quarto_session_id: cellEntry.sessionId,
778799
},
779800
source: cellEntry.source
780801
? cellEntry.source.split('\n').map((line, i, arr) => i < arr.length - 1 ? line + '\n' : line)
@@ -808,6 +829,7 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
808829
label: cellEntry.label,
809830
source: cellEntry.source,
810831
outputs: [...cellEntry.outputs],
832+
fromCurrentSession: cellEntry.sessionId === this._sessionId,
811833
});
812834
}
813835

@@ -841,6 +863,7 @@ export class QuartoOutputCacheService extends Disposable implements IQuartoOutpu
841863
label: cell.metadata?.quarto_label,
842864
source,
843865
outputs,
866+
fromCurrentSession: cell.metadata?.quarto_session_id === this._sessionId,
844867
});
845868
}
846869

src/vs/workbench/contrib/positronQuarto/browser/quartoOutputManager.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,18 @@ export class QuartoOutputContribution extends Disposable implements IEditorContr
296296
this._documentUri.scheme === 'file' &&
297297
this._isQuartoDocument()) {
298298
this._transferCacheFromUntitled(previousUri, this._documentUri);
299+
300+
// Save-as keeps the same session's outputs live: move the
301+
// stash captured above to the new URI so _loadCachedOutputs
302+
// restores the outputs as-is. Without this it would re-read
303+
// them from the just-transferred cache and mark them
304+
// restoredFromCache, wrongly hiding Fix/Explain on errors the
305+
// user produced this session.
306+
const stash = this._stashedOutputsByUri.get(previousUri.toString());
307+
if (stash) {
308+
this._stashedOutputsByUri.delete(previousUri.toString());
309+
this._stashedOutputsByUri.set(this._documentUri.toString(), stash);
310+
}
299311
}
300312

301313
// Reset initialization flags so we can re-initialize for the new document
@@ -729,16 +741,15 @@ export class QuartoOutputContribution extends Disposable implements IEditorContr
729741
);
730742
const source = model.getValueInRange(codeRange);
731743

732-
// Save outputs to cache under the new file URI
744+
// Save outputs to cache under the new file URI. The in-memory
745+
// output state is restored separately by _loadCachedOutputs:
746+
// from the re-keyed stash when the outputs were live in this
747+
// session, or from this transferred cache otherwise. Seeding
748+
// _outputsByCell here as well would duplicate every output.
733749
for (const output of outputs) {
734750
this._cacheService.saveOutput(toUri, cell.id, cell.contentHash, cell.label, output, source);
735751
}
736752

737-
// Also restore the in-memory output state so view zones can be created
738-
// This is needed because onDidChangeModel clears _outputsByCell before calling this method
739-
this._outputsByCell.set(cell.id, [...outputs]);
740-
this._contentHashByCellId.set(cell.id, cell.contentHash);
741-
742753
transferredCount++;
743754
this._logService.debug('[QuartoOutputContribution] Transferred outputs for cell', cellId, '-> new cell', cell.id);
744755
} else {
@@ -845,11 +856,13 @@ export class QuartoOutputContribution extends Disposable implements IEditorContr
845856

846857
// Restore outputs for this cell
847858
for (const cachedOutput of cachedCell.outputs) {
848-
// Mark the output as cache-restored so stale-sensitive
849-
// actions (e.g. Fix/Explain on errors) are suppressed.
850-
// The marked copy is what we store, so the flag survives
851-
// tab-switch stashes too.
852-
const output: ICellOutput = { ...cachedOutput, restoredFromCache: true };
859+
// Mark outputs that survive from a previous session so
860+
// stale-sensitive actions (e.g. Fix/Explain on errors) are
861+
// suppressed; outputs the cache saw written this session
862+
// (e.g. re-read after a save-as) stay fresh. The marked
863+
// copy is what we store, so the flag survives tab-switch
864+
// stashes too.
865+
const output: ICellOutput = { ...cachedOutput, restoredFromCache: !cachedCell.fromCurrentSession };
853866

854867
// Store in memory
855868
const outputs = this._outputsByCell.get(cell.id) ?? [];
@@ -1047,8 +1060,13 @@ export class QuartoOutputContribution extends Disposable implements IEditorContr
10471060
}
10481061
};
10491062

1050-
// Resolve the failing code chunk for the assistant quick-fix buttons
1051-
viewZone.getCellContext = () => this._resolveCellContext(cellId);
1063+
// Resolve the failing code chunk for the assistant quick-fix buttons.
1064+
// The view zone's cell ID is looked up at call time so the closure
1065+
// stays valid when _updateViewZonePositionsImmediate remaps IDs.
1066+
viewZone.getCellContext = () => {
1067+
const currentCellId = this._findCellIdForViewZone(viewZone);
1068+
return currentCellId !== undefined ? this._resolveCellContext(currentCellId) : undefined;
1069+
};
10521070

10531071
// Set up copy handler
10541072
this._outputHandlingDisposables.add(viewZone.onCopyRequested(request => {
@@ -1093,6 +1111,20 @@ export class QuartoOutputContribution extends Disposable implements IEditorContr
10931111
return viewZone;
10941112
}
10951113

1114+
/**
1115+
* Reverse-lookup a view zone's current cell ID. Cell IDs embed the cell's
1116+
* index, so _updateViewZonePositionsImmediate re-keys view zones when
1117+
* cells move; callbacks that need the ID must resolve it at call time.
1118+
*/
1119+
private _findCellIdForViewZone(viewZone: QuartoOutputViewZone): string | undefined {
1120+
for (const [cellId, zone] of this._viewZones) {
1121+
if (zone === viewZone) {
1122+
return cellId;
1123+
}
1124+
}
1125+
return undefined;
1126+
}
1127+
10961128
/**
10971129
* Resolve the code chunk behind a cell's output so the assistant
10981130
* quick-fix buttons can attach the failing code and its location.
@@ -1653,10 +1685,6 @@ export class QuartoOutputContribution extends Disposable implements IEditorContr
16531685
// Add with new ID
16541686
if (viewZone) {
16551687
this._viewZones.set(newId, viewZone);
1656-
// Rebind the quick-fix context resolver: the closure set in
1657-
// _createViewZone captured the old cell ID, whose bookkeeping
1658-
// entries are deleted just above.
1659-
viewZone.getCellContext = () => this._resolveCellContext(newId);
16601688
}
16611689
if (outputs) {
16621690
this._outputsByCell.set(newId, outputs);

src/vs/workbench/contrib/positronQuarto/browser/quartoOutputViewZone.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,6 @@ export class QuartoOutputViewZone extends Disposable implements IViewZone {
197197
// React renderers for inline data explorer outputs
198198
private readonly _reactRenderersByOutputId = new Map<string, PositronReactRenderer>();
199199

200-
// Quick-fix mount points by renderer key, so a replaced mount's container
201-
// can be removed from the DOM along with its disposed renderer.
202-
private readonly _quickFixContainersByKey = new Map<string, HTMLElement>();
203-
204200
// Configuration service for reading settings
205201
private readonly _configurationService: IConfigurationService | undefined;
206202

@@ -1119,9 +1115,6 @@ export class QuartoOutputViewZone extends Disposable implements IViewZone {
11191115
renderer.dispose();
11201116
}
11211117
this._reactRenderersByOutputId.clear();
1122-
// The container elements are removed with the broader DOM teardown;
1123-
// this just drops the stale references.
1124-
this._quickFixContainersByKey.clear();
11251118
}
11261119

11271120
/**
@@ -2532,26 +2525,30 @@ export class QuartoOutputViewZone extends Disposable implements IViewZone {
25322525
// availability and renders nothing when the assistant is unavailable.
25332526
// The renderer is registered so _disposeAllReactRenderers tears it down
25342527
// when outputs are cleared or the view zone is disposed.
2535-
const quickFixContainer = document.createElement('div');
2536-
quickFixContainer.className = 'quarto-output-error-quick-fix';
2537-
container.appendChild(quickFixContainer);
2538-
2539-
const renderer = new PositronReactRenderer(quickFixContainer);
25402528
// _renderError runs once per error item, so an output with multiple
2541-
// error items would reuse this key; dispose any renderer it replaces
2542-
// and remove the replaced mount's now-empty container.
2529+
// error items would reuse this key; only the first item mounts the
2530+
// buttons.
25432531
const rendererKey = `${output.outputId}::quick-fix`;
2544-
this._reactRenderersByOutputId.get(rendererKey)?.dispose();
2545-
this._quickFixContainersByKey.get(rendererKey)?.remove();
2546-
this._quickFixContainersByKey.set(rendererKey, quickFixContainer);
2547-
this._reactRenderersByOutputId.set(rendererKey, renderer);
2548-
renderer.render(
2549-
React.createElement(QuartoOutputQuickFix, {
2550-
errorContent: errorText,
2551-
getCellContext: () => this._getCellContext?.(),
2552-
restoredFromCache: output.restoredFromCache,
2553-
})
2554-
);
2532+
if (!this._reactRenderersByOutputId.has(rendererKey)) {
2533+
const quickFixContainer = document.createElement('div');
2534+
quickFixContainer.className = 'quarto-output-error-quick-fix';
2535+
// The buttons mount asynchronously (React commit, assistant
2536+
// availability flipping) inside the output element's role='log'
2537+
// live region; suppress announcements for this subtree so their
2538+
// insertion isn't read out after the error alert.
2539+
quickFixContainer.setAttribute('aria-live', 'off');
2540+
container.appendChild(quickFixContainer);
2541+
2542+
const renderer = new PositronReactRenderer(quickFixContainer);
2543+
this._reactRenderersByOutputId.set(rendererKey, renderer);
2544+
renderer.render(
2545+
React.createElement(QuartoOutputQuickFix, {
2546+
errorContent: errorText,
2547+
getCellContext: () => this._getCellContext?.(),
2548+
restoredFromCache: output.restoredFromCache,
2549+
})
2550+
);
2551+
}
25552552

25562553
return container;
25572554
}

src/vs/workbench/contrib/positronQuarto/common/quartoExecutionTypes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ export interface ICachedCellOutput {
298298
readonly source?: string;
299299
/** Stored outputs */
300300
readonly outputs: ICellOutput[];
301+
/**
302+
* Whether this cell's outputs were last written to the cache during the
303+
* current window session. False for outputs restored from a previous
304+
* session, whose stale-sensitive actions (e.g. Fix/Explain on errors)
305+
* should be suppressed.
306+
*/
307+
readonly fromCurrentSession: boolean;
301308
}
302309

303310
/**

0 commit comments

Comments
 (0)