|
61 | 61 | const errorText = document.getElementById('errorText');
|
62 | 62 | const toolbar = document.getElementById('toolbar');
|
63 | 63 | const statusBar = document.getElementById('statusBar');
|
| 64 | + const progressBarOverlay = document.getElementById('progressBar'); |
| 65 | + const progressBar = document.querySelector('#progressBar .progress'); |
64 | 66 | const originalStatus = document.getElementById('originalStatus');
|
65 | 67 | const generatedStatus = document.getElementById('generatedStatus');
|
66 | 68 |
|
|
579 | 581 | const toolbarHeight = 32;
|
580 | 582 | const statusBarHeight = 32;
|
581 | 583 |
|
582 |
| - function finishLoading(code, map) { |
| 584 | + function waitForDOM() { |
| 585 | + return new Promise(r => setTimeout(r, 1)); |
| 586 | + } |
| 587 | + |
| 588 | + async function finishLoading(code, map) { |
583 | 589 | const startTime = Date.now();
|
584 | 590 | promptText.style.display = 'none';
|
585 | 591 | toolbar.style.display = 'flex';
|
586 | 592 | statusBar.style.display = 'flex';
|
587 | 593 | canvas.style.display = 'block';
|
| 594 | + originalStatus.textContent = generatedStatus.textContent = ''; |
| 595 | + fileList.innerHTML = ''; |
| 596 | + const option = document.createElement('option'); |
| 597 | + option.textContent = `Loading...`; |
| 598 | + fileList.appendChild(option); |
| 599 | + fileList.disabled = true; |
| 600 | + fileList.selectedIndex = 0; |
| 601 | + originalTextArea = generatedTextArea = hover = null; |
| 602 | + isInvalid = true; |
588 | 603 | updateHash(code, map);
|
| 604 | + |
| 605 | + // Let the browser update before parsing the source map, which may be slow |
| 606 | + await waitForDOM(); |
589 | 607 | const sm = parseSourceMap(map);
|
590 | 608 |
|
591 |
| - // Populate the file picker |
592 |
| - fileList.innerHTML = ''; |
593 |
| - for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) { |
594 |
| - const option = document.createElement('option'); |
595 |
| - option.textContent = `${i}: ${sources[i].name}`; |
596 |
| - fileList.appendChild(option); |
597 |
| - } |
| 609 | + // Show a progress bar if this is is going to take a while |
| 610 | + let charsSoFar = 0; |
| 611 | + let progressCalls = 0; |
| 612 | + let isProgressVisible = false; |
| 613 | + const progressStart = Date.now(); |
| 614 | + const totalChars = code.length + (sm.sources.length > 0 ? sm.sources[0].content.length : 0); |
| 615 | + const progress = chars => { |
| 616 | + charsSoFar += chars; |
| 617 | + if (!isProgressVisible && progressCalls++ > 2 && charsSoFar) { |
| 618 | + const estimatedTimeLeftMS = (Date.now() - progressStart) / charsSoFar * (totalChars - charsSoFar); |
| 619 | + if (estimatedTimeLeftMS > 250) { |
| 620 | + progressBarOverlay.style.display = 'block'; |
| 621 | + isProgressVisible = true; |
| 622 | + } |
| 623 | + } |
| 624 | + if (isProgressVisible) { |
| 625 | + progressBar.style.transform = `scaleX(${charsSoFar / totalChars})`; |
| 626 | + return waitForDOM(); |
| 627 | + } |
| 628 | + }; |
| 629 | + progressBar.style.transform = `scaleX(0)`; |
598 | 630 |
|
599 | 631 | // Update the original text area when the source changes
|
600 | 632 | const otherSource = index => index === -1 ? null : sm.sources[index].name;
|
601 | 633 | const originalName = index => sm.names[index];
|
602 |
| - originalTextArea = null; |
| 634 | + let finalOriginalTextArea = null; |
603 | 635 | if (sm.sources.length > 0) {
|
604 |
| - const updateOriginalSource = () => { |
605 |
| - const source = sm.sources[fileList.selectedIndex]; |
606 |
| - originalTextArea = createTextArea({ |
607 |
| - sourceIndex: fileList.selectedIndex, |
| 636 | + const updateOriginalSource = (sourceIndex, progress) => { |
| 637 | + const source = sm.sources[sourceIndex]; |
| 638 | + return createTextArea({ |
| 639 | + sourceIndex, |
608 | 640 | text: source.content,
|
| 641 | + progress, |
609 | 642 | mappings: source.data,
|
610 | 643 | mappingsOffset: 3,
|
611 | 644 | otherSource,
|
|
619 | 652 | };
|
620 | 653 | },
|
621 | 654 | });
|
| 655 | + }; |
| 656 | + fileList.onchange = async () => { |
| 657 | + originalTextArea = await updateOriginalSource(fileList.selectedIndex); |
622 | 658 | isInvalid = true;
|
623 | 659 | };
|
624 |
| - fileList.onchange = updateOriginalSource; |
625 |
| - updateOriginalSource(); |
626 |
| - } else { |
627 |
| - const option = document.createElement('option'); |
628 |
| - option.textContent = `(no original code)`; |
629 |
| - option.disabled = true; |
630 |
| - fileList.appendChild(option); |
| 660 | + finalOriginalTextArea = await updateOriginalSource(0, progress); |
631 | 661 | }
|
632 |
| - fileList.selectedIndex = 0; |
633 | 662 |
|
634 |
| - generatedTextArea = createTextArea({ |
| 663 | + generatedTextArea = await createTextArea({ |
635 | 664 | sourceIndex: null,
|
636 | 665 | text: code,
|
| 666 | + progress, |
637 | 667 | mappings: sm.data,
|
638 | 668 | mappingsOffset: 0,
|
639 | 669 | otherSource,
|
|
649 | 679 | },
|
650 | 680 | });
|
651 | 681 |
|
| 682 | + // Only render the original text area once the generated text area is ready |
| 683 | + originalTextArea = finalOriginalTextArea; |
652 | 684 | isInvalid = true;
|
| 685 | + |
| 686 | + // Populate the file picker once there will be no more await points |
| 687 | + fileList.innerHTML = ''; |
| 688 | + if (sm.sources.length > 0) { |
| 689 | + for (let sources = sm.sources, i = 0, n = sources.length; i < n; i++) { |
| 690 | + const option = document.createElement('option'); |
| 691 | + option.textContent = `${i}: ${sources[i].name}`; |
| 692 | + fileList.appendChild(option); |
| 693 | + } |
| 694 | + fileList.disabled = false; |
| 695 | + } else { |
| 696 | + const option = document.createElement('option'); |
| 697 | + option.textContent = `(no original code)`; |
| 698 | + fileList.appendChild(option); |
| 699 | + } |
| 700 | + fileList.selectedIndex = 0; |
| 701 | + |
| 702 | + if (isProgressVisible) progressBarOverlay.style.display = 'none'; |
653 | 703 | const endTime = Date.now();
|
654 | 704 | console.log(`Finished loading in ${endTime - startTime}ms`);
|
655 | 705 | }
|
|
714 | 764 | let generatedTextArea;
|
715 | 765 | let hover = null;
|
716 | 766 |
|
717 |
| - function splitTextIntoLinesAndRuns(text) { |
| 767 | + async function splitTextIntoLinesAndRuns(text, progress) { |
718 | 768 | c.font = monospaceFont;
|
719 | 769 | const spaceWidth = c.measureText(' ').width;
|
720 | 770 | const spacesPerTab = 2;
|
721 | 771 | const parts = text.split(/(\r\n|\r|\n)/g);
|
722 | 772 | const unicodeWidthCache = new Map();
|
723 | 773 | const lines = [];
|
| 774 | + const progressChunkSize = 1 << 20; |
| 775 | + let prevProgressPoint = 0; |
724 | 776 | let longestLineInColumns = 0;
|
725 | 777 | let lineStartOffset = 0;
|
726 | 778 |
|
|
732 | 784 | continue;
|
733 | 785 | }
|
734 | 786 |
|
| 787 | + let nextProgressPoint = progress ? prevProgressPoint + progressChunkSize - lineStartOffset : Infinity; |
735 | 788 | let runs = [];
|
736 | 789 | let i = 0;
|
737 | 790 | let n = raw.length + 1; // Add 1 for the extra character at the end
|
|
743 | 796 | let whitespace = 0;
|
744 | 797 | let isSingleChunk = false;
|
745 | 798 |
|
| 799 | + // Update the progress bar occasionally |
| 800 | + if (i > nextProgressPoint) { |
| 801 | + await progress(lineStartOffset + i - prevProgressPoint); |
| 802 | + prevProgressPoint = lineStartOffset + i; |
| 803 | + nextProgressPoint = i + progressChunkSize; |
| 804 | + } |
| 805 | + |
746 | 806 | while (i < n) {
|
747 | 807 | let c1 = raw.charCodeAt(i);
|
748 | 808 | let c2;
|
|
858 | 918 | lineStartOffset += raw.length;
|
859 | 919 | }
|
860 | 920 |
|
| 921 | + if (prevProgressPoint < text.length && progress) { |
| 922 | + await progress(text.length - prevProgressPoint); |
| 923 | + } |
| 924 | + |
861 | 925 | return { lines, longestLineInColumns };
|
862 | 926 | }
|
863 | 927 |
|
864 |
| - function createTextArea({ sourceIndex, text, mappings, mappingsOffset, otherSource, originalName, bounds }) { |
| 928 | + async function createTextArea({ sourceIndex, text, progress, mappings, mappingsOffset, otherSource, originalName, bounds }) { |
865 | 929 | const shadowWidth = 16;
|
866 | 930 | const textPaddingX = 5;
|
867 | 931 | const textPaddingY = 1;
|
868 | 932 | const scrollbarThickness = 16;
|
869 |
| - let { lines, longestLineInColumns } = splitTextIntoLinesAndRuns(text); |
| 933 | + let { lines, longestLineInColumns } = await splitTextIntoLinesAndRuns(text, progress); |
870 | 934 | let animate = null;
|
871 | 935 | let lastLineIndex = lines.length - 1;
|
872 | 936 | let scrollX = 0;
|
|
1189 | 1253 | } else {
|
1190 | 1254 | if (originalTextArea.sourceIndex !== hover.mapping.originalSource) {
|
1191 | 1255 | fileList.selectedIndex = hover.mapping.originalSource;
|
1192 |
| - fileList.onchange(); |
| 1256 | + fileList.onchange().then(() => { |
| 1257 | + originalTextArea.scrollTo(hover.mapping.originalColumn, hover.mapping.originalLine); |
| 1258 | + }); |
| 1259 | + } else { |
| 1260 | + originalTextArea.scrollTo(hover.mapping.originalColumn, hover.mapping.originalLine); |
1193 | 1261 | }
|
1194 |
| - originalTextArea.scrollTo(hover.mapping.originalColumn, hover.mapping.originalLine); |
1195 | 1262 | }
|
1196 | 1263 | }
|
1197 | 1264 | return;
|
|
0 commit comments