| author | |
| committer | |
| log | a112bed705720e1c883d0c2a13e9175164544525 |
| tree | 98927b5b4c0a87b202bd84c11b0bcbf2a38162cc |
| parent | e14fa6bdce7e5ab857860af2962d89e2ed9a92f4 |
| signature | Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs |
thankfully, the widget formatters are called before most rendering state
is needed, but it brings up some reasonable edge cases which are now
handled better. as a result, the timers when having widgets of differing
fps are more accurate, particularly around starting and stopping them.
resolves #1023 files changed, 86 insertions(+), 29 deletions(-)
lib/log.test.ts+44-2| ... | ... | @@ -493,8 +493,9 @@ describe("log widgets", () => { |
| 493 | 493 | |
| 494 | 494 | host.writeOutput("log line;"); |
| 495 | 495 | |
| 496 | let flag = true; | |
| 496 | 497 | using _ = host.startWidget({ |
| 497 | format: ({ now }) => now > 500 ? null : `widget line one ${now}\nwidget line two`, | |
| 498 | format: ({ now }) => flag ? `widget line one ${now}\nwidget line two` : null, | |
| 498 | 499 | fps: 1, |
| 499 | 500 | }); |
| 500 | 501 | |
| ... | ... | @@ -506,8 +507,10 @@ describe("log widgets", () => { |
| 506 | 507 | "widget line one 0\nwidget line two\n", |
| 507 | 508 | ]), |
| 508 | 509 | }); |
| 510 | host.expectWithoutConsume(1000); | |
| 509 | 511 | host.writeOutput(" rest of line\n"); |
| 510 | host.expectFrame(1000, { | |
| 512 | flag = false; | |
| 513 | host.expectFrame(0, { | |
| 511 | 514 | stdout: " rest of line\n", |
| 512 | 515 | merged: testing.MockScreen.sync([ |
| 513 | 516 | ansi.cursorUp(1), |
| ... | ... | @@ -519,6 +522,45 @@ describe("log widgets", () => { |
| 519 | 522 | ]), |
| 520 | 523 | }); |
| 521 | 524 | host.cancel(); |
| 525 | _?.stop(); | |
| 526 | }); | |
| 527 | ||
| 528 | test("writing during a render function is OK", () => { | |
| 529 | const host = new testing.MockScreen(); | |
| 530 | ||
| 531 | using w = host.startWidget({ | |
| 532 | format: ({ now }) => { | |
| 533 | host.writeOutput("log line\n"); | |
| 534 | return "widget line"; | |
| 535 | }, | |
| 536 | fps: 1, | |
| 537 | }); | |
| 538 | ||
| 539 | host.expectFrame(0, { | |
| 540 | stdout: "log line\n", | |
| 541 | merged: testing.MockScreen.sync([ | |
| 542 | "log line\n", | |
| 543 | "widget line\n", | |
| 544 | ]), | |
| 545 | }); | |
| 546 | host.expectFrame(1000, { | |
| 547 | stdout: "log line\n", | |
| 548 | merged: testing.MockScreen.sync([ | |
| 549 | ansi.cursorUp(1), | |
| 550 | ansi.clearFullLine, | |
| 551 | "log line\n", | |
| 552 | "widget line\n", | |
| 553 | ]), | |
| 554 | }); | |
| 555 | w?.stop(); | |
| 556 | host.expectFrame(0, { | |
| 557 | // stdout: "log line\n", | |
| 558 | merged: testing.MockScreen.sync([ | |
| 559 | ansi.cursorUp(1), | |
| 560 | ansi.clearFullLine, | |
| 561 | ]), | |
| 562 | }); | |
| 563 | host.cancel(); | |
| 522 | 564 | }); |
| 523 | 565 | }); |
| 524 | 566 |
lib/log.ts+28-11| ... | ... | @@ -434,10 +434,11 @@ interface WidgetState { |
| 434 | 434 | export function createTerminalWidgetHost( |
| 435 | 435 | env: TerminalWidgetHostOptions, |
| 436 | 436 | ): WidgetHost { |
| 437 | const { lockTerminal, now, delay, writeOutputTemporaryLock } = env; | |
| 437 | const { lockTerminal, now, delay, writeOutputTemporaryLock, color } = env; | |
| 438 | 438 | |
| 439 | 439 | let timer: async.Cancelable<void> | null = null; |
| 440 | 440 | |
| 441 | let rendering = false; | |
| 441 | 442 | let locks = 0; |
| 442 | 443 | let redrawTime = 0; |
| 443 | 444 | let lastFlush = 0; |
| ... | ... | @@ -454,6 +455,8 @@ export function createTerminalWidgetHost( |
| 454 | 455 | |
| 455 | 456 | function redrawCallback() { |
| 456 | 457 | timer = null; |
| 458 | ASSERT(!rendering); | |
| 459 | rendering = true; | |
| 457 | 460 | redrawTime = (lastFlush = now()) - 0.00001; // windows time precision workaround |
| 458 | 461 | |
| 459 | 462 | // trivial path when not using widgets |
| ... | ... | @@ -479,6 +482,7 @@ export function createTerminalWidgetHost( |
| 479 | 482 | } |
| 480 | 483 | partialLineIndex = partialLineLength(buffer); |
| 481 | 484 | buffer = ""; |
| 485 | rendering = false | |
| 482 | 486 | return; |
| 483 | 487 | } |
| 484 | 488 | |
| ... | ... | @@ -488,11 +492,17 @@ export function createTerminalWidgetHost( |
| 488 | 492 | let newWidgetLines: string[] = []; |
| 489 | 493 | let next = Infinity; |
| 490 | 494 | for (let w = 0, { length } = widgets; w < length; w += 1) { |
| 491 | const out = UNWRAP(widgets[w]).format({ | |
| 492 | now: lastFlush, | |
| 493 | width: columns, | |
| 494 | height: rows, | |
| 495 | }); | |
| 495 | const widget = UNWRAP(widgets[w]); | |
| 496 | let out: string | { text: string } | null; | |
| 497 | try { | |
| 498 | out = widget.format({ | |
| 499 | now: lastFlush, | |
| 500 | width: columns, | |
| 501 | height: rows, | |
| 502 | }); | |
| 503 | } catch (e) { | |
| 504 | out = e instanceof Error ? stack.format(e, color) : errors.message(e); | |
| 505 | } | |
| 496 | 506 | if (!out) { |
| 497 | 507 | widgets.splice(w, 1); |
| 498 | 508 | UNWRAP(internals.splice(w, 1)[0]); |
| ... | ... | @@ -513,6 +523,8 @@ export function createTerminalWidgetHost( |
| 513 | 523 | newWidgetLines = newWidgetLines.slice(0, rows - 1); |
| 514 | 524 | if (next < Infinity) redrawSoon(next); |
| 515 | 525 | |
| 526 | terminal ??= lockTerminal(); | |
| 527 | ||
| 516 | 528 | if (!newWidgetLines[0]) { |
| 517 | 529 | ASSERT(!needsToSaveCursor); |
| 518 | 530 | if (lines.length > 0) { |
| ... | ... | @@ -532,6 +544,7 @@ export function createTerminalWidgetHost( |
| 532 | 544 | if (buffer) terminal.writeOutput(buffer); |
| 533 | 545 | buffer = ""; |
| 534 | 546 | if (hasSyncStart) terminal.writeInteractive(ansi.syncEnd); |
| 547 | rendering = false; | |
| 535 | 548 | return; |
| 536 | 549 | } |
| 537 | 550 | |
| ... | ... | @@ -631,19 +644,19 @@ export function createTerminalWidgetHost( |
| 631 | 644 | needsToRestoreCursor ||= needsToSaveCursor; |
| 632 | 645 | needsToSaveCursor = false; |
| 633 | 646 | lines = newWidgetLines; |
| 634 | ||
| 635 | 647 | buffer = ""; |
| 648 | rendering = false; | |
| 636 | 649 | } |
| 637 | 650 | |
| 638 | 651 | function redrawSoon(ms: number) { |
| 639 | if (locks > 0 || (ms === 0 && timer)) return; | |
| 652 | if (locks > 0 || (ms === 0 && rendering)) return; | |
| 640 | 653 | const newRedrawTime = now() + ms; |
| 641 | 654 | if (timer) { |
| 642 | 655 | if (redrawTime < newRedrawTime) return; |
| 643 | 656 | timer.cancel(); // cancel previous |
| 644 | timer = null; | |
| 645 | 657 | } |
| 646 | redrawTime = newRedrawTime + 1; | |
| 658 | // 1ms wiggle room, generally runtimes have much larger variance on timers | |
| 659 | redrawTime = newRedrawTime - 1; | |
| 647 | 660 | timer = delay(ms); |
| 648 | 661 | timer.then(redrawCallback); |
| 649 | 662 | } |
| ... | ... | @@ -690,6 +703,7 @@ export function createTerminalWidgetHost( |
| 690 | 703 | if (chunk) buffer += chunk, redrawSoon(0); |
| 691 | 704 | }, |
| 692 | 705 | getDrawLock(mode) { |
| 706 | if (rendering) ASSERT(locks === 0); | |
| 693 | 707 | if (locks === 0) { |
| 694 | 708 | flushAndClear(mode === "short"); |
| 695 | 709 | if (widgets.length > 0 && terminal) { |
| ... | ... | @@ -756,12 +770,14 @@ export function createTerminalWidgetHost( |
| 756 | 770 | }; |
| 757 | 771 | }, |
| 758 | 772 | cancel() { |
| 773 | ASSERT(!rendering, "cannot call cancel() during rendering"); | |
| 759 | 774 | flushAndClear(false); |
| 760 | 775 | widgets.splice(0, widgets.length); |
| 776 | internals.splice(0, internals.length); | |
| 761 | 777 | }, |
| 762 | 778 | delay, |
| 763 | 779 | now, |
| 764 | capabilities: env.color ? ["widget", "color"] : ["widget"], | |
| 780 | capabilities: color ? ["widget", "color"] : ["widget"], | |
| 765 | 781 | }; |
| 766 | 782 | } |
| 767 | 783 | |
| ... | ... | @@ -1200,6 +1216,7 @@ export type MessageFormatFunction = ( |
| 1200 | 1216 | ) => string; |
| 1201 | 1217 | |
| 1202 | 1218 | import { ASSERT, UNWRAP } from "./assert.ts"; |
| 1219 | import * as errors from "./error.ts"; | |
| 1203 | 1220 | import * as async from "./async.ts"; |
| 1204 | 1221 | import * as stack from "./log/stack.ts"; |
| 1205 | 1222 | import * as node from "./node.ts"; |
lib/testing.ts+14-16| ... | ... | @@ -116,7 +116,6 @@ export class FakeTimers { |
| 116 | 116 | resolve: () => void; |
| 117 | 117 | src: stack.Frame[]; |
| 118 | 118 | }> = []; |
| 119 | waitTime = 0; | |
| 120 | 119 | |
| 121 | 120 | now: () => number = () => { |
| 122 | 121 | return this.time; |
| ... | ... | @@ -127,11 +126,10 @@ export class FakeTimers { |
| 127 | 126 | new SyncPromise((resolve) => { |
| 128 | 127 | ASSERT(this.entries.length === 0); |
| 129 | 128 | this.entries.push({ |
| 130 | duration: ms - this.waitTime, | |
| 129 | duration: ms, | |
| 131 | 130 | resolve, |
| 132 | 131 | src, |
| 133 | 132 | }); |
| 134 | this.waitTime = ms; | |
| 135 | 133 | }), |
| 136 | 134 | () => { |
| 137 | 135 | ASSERT(this.entries.length === 1); |
| ... | ... | @@ -245,7 +243,17 @@ export class MockScreen implements Disposable, log.WidgetHost { |
| 245 | 243 | } |
| 246 | 244 | |
| 247 | 245 | expectWithoutConsume(ms: number) { |
| 248 | ASSERT(UNWRAP(this.timers.entries[0]).duration === 0); | |
| 246 | const wait = UNWRAP( | |
| 247 | this.timers.entries[0], | |
| 248 | () => this.out.length > 0 ? "terminal i/o did not wait" : "no terminal i/o", | |
| 249 | ); | |
| 250 | ASSERT( | |
| 251 | ms === wait.duration, | |
| 252 | `expected ${ms}ms to pass, got ${wait.duration}, from:\n${ | |
| 253 | wait.src.map((frame) => stack.formatFrame(frame, true)).join("\n") | |
| 254 | }`, | |
| 255 | ); | |
| 256 | return wait; | |
| 249 | 257 | } |
| 250 | 258 | |
| 251 | 259 | expectFrame(ms: number | null, { stdout, stderr, merged: out }: { |
| ... | ... | @@ -254,19 +262,9 @@ export class MockScreen implements Disposable, log.WidgetHost { |
| 254 | 262 | merged?: string; |
| 255 | 263 | }) { |
| 256 | 264 | if (ms != null) { |
| 257 | const wait = UNWRAP( | |
| 258 | this.timers.entries.shift(), | |
| 259 | () => this.out.length > 0 ? "terminal i/o did not wait" : "no terminal i/o", | |
| 260 | ); | |
| 261 | ASSERT( | |
| 262 | ms === wait.duration, | |
| 263 | `expected ${ms}ms to pass, got ${wait.duration}, from:\n${ | |
| 264 | wait.src.map((frame) => stack.formatFrame(frame, true)).join("\n") | |
| 265 | }`, | |
| 266 | ); | |
| 267 | ||
| 265 | const wait = this.expectWithoutConsume(ms); | |
| 266 | this.timers.entries.shift(); | |
| 268 | 267 | this.timers.time += wait.duration; |
| 269 | this.timers.waitTime = 0; | |
| 270 | 268 | wait.resolve(); |
| 271 | 269 | } else { |
| 272 | 270 | ASSERT(this.timers.entries.length === 0); |