-
Notifications
You must be signed in to change notification settings - Fork 494
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 3 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,8 +172,10 @@ export class KeywordHighlighter implements IDisposable { | |
| if (this.enterInputPending) { | ||
| this.scheduleEnterInputIdleClear(); | ||
| } | ||
| const isBrowsingScrollback = this.isBrowsingScrollback(); | ||
| const outputDrivenPendingScroll = | ||
| this.pendingRefreshReason === "scroll" | ||
| && !isBrowsingScrollback | ||
| && ( | ||
| this.hasOutputPositionChangedSinceLastSnapshot() | ||
| || this.hasDecorationMarkerShiftSinceLastRefresh() | ||
|
|
@@ -706,9 +715,22 @@ 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) { | ||
| if (this.pendingRefreshReason === "scroll") { | ||
| this.cancelQueuedRefreshSchedule(); | ||
| this.pendingRefreshReason = "write"; | ||
| } | ||
| this.markVisibleRangeDirty(); | ||
| return; | ||
|
Comment on lines
+733
to
+734
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 the remote PTY produces no write after Enter—for example, with echo disabled or a stalled connection— Useful? React with 👍 / 👎. |
||
| } | ||
| const now = performance.now(); | ||
| const buffer = this.term.buffer.active; | ||
| const isBrowsingScrollback = buffer.viewportY < buffer.baseY; | ||
| const isOutputDrivenViewportChange = | ||
| !isBrowsingScrollback && | ||
| this.lastWriteAt > 0 && | ||
|
|
@@ -1061,7 +1083,16 @@ 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 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 a user reaches the bottom while the Enter guard is active—for example, by pressing End after first scrolling upward—
viewportY === baseYmakes this branch classify the user action as Enter-driven output. It marks the viewport dirty and returns without scanning it; if no write follows, the newly revealed bottom viewport is not refreshed until the 600 ms guard timer plus the debounce delay expires. Distinguish an actual output-driven viewport change from a user scroll that happens to end at the bottom so the latter retains the synchronous scroll refresh path.Useful? React with 👍 / 👎.