-
Notifications
You must be signed in to change notification settings - Fork 484
fix(terminal): defer keyword scroll refresh while Enter is pending #2880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
315e596
4935066
6c4e27c
fbaa038
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,6 +134,7 @@ export class KeywordHighlighter implements IDisposable { | |
| private static readonly WRITE_BURST_DEBOUNCE_MS = 180; | ||
| private static readonly WRITE_BURST_IMMEDIATE_MIN_INTERVAL_MS = 48; | ||
| private static readonly WRITE_BURST_HIGHLIGHT_PAUSE_MS = 260; | ||
| private static readonly ENTER_INPUT_GUARD_MS = 600; | ||
| private static readonly WRITE_PRUNE_IDLE_MS = 600; | ||
|
|
||
| constructor(term: XTerm) { | ||
|
|
@@ -153,9 +154,15 @@ export class KeywordHighlighter implements IDisposable { | |
| if (data.includes("\r") || data.includes("\n")) { | ||
| this.enterInputPending = true; | ||
| this.enterQueuedWriteCancellationPending = true; | ||
| if (this.enterInputIdleTimer) { | ||
| clearTimeout(this.enterInputIdleTimer); | ||
| this.enterInputIdleTimer = null; | ||
| // Time-bound Enter protection even when no echo/write arrives (echo | ||
| // off, stalled PTY). onWriteParsed re-arms this on each write. | ||
| this.scheduleEnterInputIdleClear(); | ||
| // Drop any pending user-scroll refresh so Enter echo cannot finish a | ||
| // scroll pass that rescans/repaints still-visible keyword decorations | ||
| // before onWriteParsed owns the write path (Ubuntu RTT). | ||
| if (this.pendingRefreshReason === "scroll" && !this.isBrowsingScrollback()) { | ||
| this.cancelQueuedRefreshSchedule(); | ||
| this.pendingRefreshReason = "write"; | ||
| } | ||
| } | ||
| }), | ||
|
|
@@ -165,11 +172,12 @@ export class KeywordHighlighter implements IDisposable { | |
| if (this.enterInputPending) { | ||
| this.scheduleEnterInputIdleClear(); | ||
| } | ||
| const isBrowsingScrollback = this.isBrowsingScrollback(); | ||
| const outputDrivenPendingScroll = | ||
| this.pendingRefreshReason === "scroll" | ||
| && !isBrowsingScrollback | ||
| && ( | ||
| this.hasOutputPositionChangedSinceLastSnapshot() | ||
| || this.hasDecorationMarkerShiftSinceLastRefresh() | ||
| this.hasOutputDrivenViewportChange() | ||
| ); | ||
| const cancelQueuedWriteForEnter = | ||
| this.enterQueuedWriteCancellationPending | ||
|
|
@@ -706,9 +714,26 @@ export class KeywordHighlighter implements IDisposable { | |
| } | ||
|
|
||
| private triggerViewportChangeRefresh() { | ||
| const isBrowsingScrollback = this.isBrowsingScrollback(); | ||
| // Enter echo often emits onScroll before onWriteParsed. After an idle gap | ||
| // lastWriteAt looks stale and lastRenderRange is usually null (cleared by | ||
| // the previous write refresh), so the output-driven scroll path would | ||
| // synchronously rescan the viewport and flash keywords still on screen. | ||
| // Keep real scrollback browsing synchronous; only defer the bottom-pinned | ||
| // viewport movement that can be caused by the pending Enter echo. | ||
| if ( | ||
| this.enterInputPending | ||
| && !isBrowsingScrollback | ||
| && this.hasOutputDrivenViewportChange() | ||
| ) { | ||
| if (this.pendingRefreshReason === "scroll") { | ||
| this.cancelQueuedRefreshSchedule(); | ||
| this.pendingRefreshReason = "write"; | ||
| } | ||
| this.markVisibleRangeDirty(); | ||
| return; | ||
| } | ||
| const now = performance.now(); | ||
| const buffer = this.term.buffer.active; | ||
| const isBrowsingScrollback = buffer.viewportY < buffer.baseY; | ||
| const isOutputDrivenViewportChange = | ||
| !isBrowsingScrollback && | ||
| this.lastWriteAt > 0 && | ||
|
|
@@ -1061,7 +1086,23 @@ export class KeywordHighlighter implements IDisposable { | |
| this.enterInputIdleTimer = setTimeout(() => { | ||
| this.enterInputIdleTimer = null; | ||
| this.enterInputPending = false; | ||
| }, KeywordHighlighter.WRITE_PRUNE_IDLE_MS); | ||
| // Catch up any viewport motion deferred while Enter protection blocked | ||
| // scroll refresh (e.g. user scrolled during the post-Enter window). | ||
| this.markVisibleRangeDirty(); | ||
| this.triggerRefresh("debounced", "write"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Enter launches output that trips large-output pressure while scrollback is saturated, the pressure state intentionally remains active for two Useful? React with 👍 / 👎. |
||
| }, KeywordHighlighter.ENTER_INPUT_GUARD_MS); | ||
| } | ||
|
|
||
| private isBrowsingScrollback(): boolean { | ||
| const buffer = this.term.buffer.active; | ||
| return buffer.viewportY < buffer.baseY; | ||
| } | ||
|
|
||
| private hasOutputDrivenViewportChange(): boolean { | ||
| return ( | ||
| this.hasOutputPositionChangedSinceLastSnapshot() | ||
| || this.hasDecorationMarkerShiftSinceLastRefresh() | ||
| ); | ||
| } | ||
|
|
||
| private mergeRefreshReason(current: RefreshReason, next: RefreshReason): RefreshReason { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the remote PTY produces no write after Enter—for example, with echo disabled or a stalled connection—
onWriteParsednever runs.enterInputPendingis set byonData, but its only clear timer is armed insideonWriteParsed, so this new early return suppresses every subsequent user-scroll refresh indefinitely. Newly revealed scrollback lines therefore remain unscanned and unhighlighted until unrelated output arrives; arm a fallback timer fromonDataor otherwise time-bound this guard.Useful? React with 👍 / 👎.