Skip to content

Commit 2ae4e5f

Browse files
rodbegbieclaude
andcommitted
fix(lint-framework): coalesce lint requests dropped while one is in flight
`requestLintUpdate` used `lintRequested` as a single-flight mutex, but a request arriving while one was in flight was discarded rather than queued. The default delay is 0, so there is no debounce to coalesce them either. A burst of input could therefore leave the rendered lints belonging to a stale prefix of the text, recovered only by the 1000ms safety-net timer. That staleness is invisible on screen, because `remapLintToCurrentSource` keeps the highlight correctly positioned. It is not invisible to the ignore path: `LintContext` hashes the tokens following a lint, so a lint computed against a prefix carries a `context_hash` that never matches the one derived from the final text. Dismissing such a lint records a hash that matches nothing, and the highlight comes straight back and stays. Track a `lintDirty` flag and re-run once after the in-flight pass releases the mutex. The re-run must happen after that release, or it hits the same guard and is dropped in turn. Also wrap the lint in try/finally. A rejected `lintProvider` previously left `lintRequested` stuck at true, permanently stopping all linting. Refs #3911 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Entire-Checkpoint: b3fb75386990
1 parent 0262582 commit 2ae4e5f

1 file changed

Lines changed: 49 additions & 32 deletions

File tree

packages/lint-framework/src/lint/LintFramework.ts

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export default class LintFramework {
4848
private targets: Set<Node>;
4949
private scrollableAncestors: Set<HTMLElement>;
5050
private lintRequested = false;
51+
/** Set when a lint request arrives while one is already in flight. */
52+
private lintDirty = false;
5153
private renderRequested = false;
5254
private lintDelayTimer: number | null = null;
5355
private lastInputAt = 0;
@@ -149,51 +151,66 @@ export default class LintFramework {
149151
// Avoid duplicate requests in the queue
150152
if (!immediate) {
151153
this.lintRequested = true;
154+
// This pass reads the text as it is now, so it already covers every
155+
// request made before it started.
156+
this.lintDirty = false;
152157
}
153158

154-
const lintResults = await Promise.all(
155-
this.onScreenTargets().map(async (target) => {
156-
if (!document.contains(target)) {
157-
this.targets.delete(target);
158-
return { target: null as HTMLElement | null, lints: {} };
159-
}
159+
try {
160+
const lintResults = await Promise.all(
161+
this.onScreenTargets().map(async (target) => {
162+
if (!document.contains(target)) {
163+
this.targets.delete(target);
164+
return { target: null as HTMLElement | null, lints: {} };
165+
}
160166

161-
const { text, isCM, newLineIndices } = this.getTargetText(target);
167+
const { text, isCM, newLineIndices } = this.getTargetText(target);
162168

163-
if (!text || text.length > 120000) {
164-
return { target: null as HTMLElement | null, lints: {} };
165-
}
169+
if (!text || text.length > 120000) {
170+
return { target: null as HTMLElement | null, lints: {} };
171+
}
166172

167-
const language = getTargetLanguage(target);
168-
let lintsBySource = await this.lintProvider(text, window.location.hostname, {
169-
forceAllHeadings: isHeading(target),
170-
language,
171-
});
173+
const language = getTargetLanguage(target);
174+
let lintsBySource = await this.lintProvider(text, window.location.hostname, {
175+
forceAllHeadings: isHeading(target),
176+
language,
177+
});
172178

173-
if (isCM) {
174-
// We're about to modify a reference, so let's work on a copy.
175-
lintsBySource = window.structuredClone(lintsBySource);
179+
if (isCM) {
180+
// We're about to modify a reference, so let's work on a copy.
181+
lintsBySource = window.structuredClone(lintsBySource);
176182

177-
for (const lints of Object.values(lintsBySource)) {
178-
for (const lint of lints) {
179-
const offset_start = newLineIndices.findIndex((i) => i > lint.span.start);
180-
const offset_end = newLineIndices.findIndex((i) => i > lint.span.end);
183+
for (const lints of Object.values(lintsBySource)) {
184+
for (const lint of lints) {
185+
const offset_start = newLineIndices.findIndex((i) => i > lint.span.start);
186+
const offset_end = newLineIndices.findIndex((i) => i > lint.span.end);
181187

182-
lint.span.start -= offset_start;
183-
lint.span.end -= offset_end;
188+
lint.span.start -= offset_start;
189+
lint.span.end -= offset_end;
190+
}
184191
}
185192
}
186-
}
187193

188-
return { target: target as HTMLElement, lints: lintsBySource };
189-
}),
190-
);
194+
return { target: target as HTMLElement, lints: lintsBySource };
195+
}),
196+
);
191197

192-
this.lastLints = lintResults.filter((r) => r.target != null) as any;
193-
if (!immediate) {
194-
this.lintRequested = false;
198+
this.lastLints = lintResults.filter((r) => r.target != null) as any;
199+
this.requestRender();
200+
} finally {
201+
if (!immediate) {
202+
this.lintRequested = false;
203+
}
204+
}
205+
206+
// Must run after the flag above is cleared, otherwise this call hits the
207+
// same guard, marks the framework dirty again and is dropped.
208+
if (!immediate && this.lintDirty) {
209+
this.lintDirty = false;
210+
void this.requestLintUpdate();
195211
}
196-
this.requestRender();
212+
} else if (!immediate && this.lintRequested) {
213+
this.lintDirty = true;
197214
}
198215
}
199216

0 commit comments

Comments
 (0)