|
772 | 772 | const unicodeWidthCache = new Map(); |
773 | 773 | const lines = []; |
774 | 774 | const progressChunkSize = 1 << 20; |
| 775 | + let runData = new Int32Array(1024); |
| 776 | + let runDataLength = 0; |
775 | 777 | let prevProgressPoint = 0; |
776 | 778 | let longestLineInColumns = 0; |
777 | 779 | let lineStartOffset = 0; |
|
784 | 786 | continue; |
785 | 787 | } |
786 | 788 |
|
| 789 | + const runBase = runDataLength; |
| 790 | + const n = raw.length + 1; // Add 1 for the extra character at the end |
787 | 791 | let nextProgressPoint = progress ? prevProgressPoint + progressChunkSize - lineStartOffset : Infinity; |
788 | | - let runs = []; |
789 | 792 | let i = 0; |
790 | | - let n = raw.length + 1; // Add 1 for the extra character at the end |
791 | 793 | let column = 0; |
792 | 794 |
|
793 | 795 | while (i < n) { |
|
904 | 906 | i++; |
905 | 907 | } |
906 | 908 |
|
907 | | - runs.push({ |
908 | | - whitespaceAndFlags: whitespace | (isSingleChunk ? 0x100 /* isSingleChunk */ : 0), |
909 | | - startIndex, |
910 | | - endIndex: i, |
911 | | - startColumn, |
912 | | - endColumn: column, |
913 | | - }); |
| 909 | + // Append the run to the typed array |
| 910 | + if (runDataLength + 5 > runData.length) { |
| 911 | + const newData = new Int32Array(runData.length << 1); |
| 912 | + newData.set(runData); |
| 913 | + runData = newData; |
| 914 | + } |
| 915 | + runData[runDataLength] = whitespace | (isSingleChunk ? 0x100 /* isSingleChunk */ : 0); |
| 916 | + runData[runDataLength + 1] = startIndex; |
| 917 | + runData[runDataLength + 2] = i; |
| 918 | + runData[runDataLength + 3] = startColumn; |
| 919 | + runData[runDataLength + 4] = column; |
| 920 | + runDataLength += 5; |
914 | 921 | } |
915 | 922 |
|
916 | | - lines.push({ raw, runs, runsText: {}, endIndex: i, endColumn: column }); |
| 923 | + const runCount = (runDataLength - runBase) / 5; |
| 924 | + lines.push({ raw, runBase, runCount, runText: {}, endIndex: i, endColumn: column }); |
917 | 925 | longestLineInColumns = Math.max(longestLineInColumns, column); |
918 | 926 | lineStartOffset += raw.length; |
919 | 927 | } |
|
922 | 930 | await progress(text.length - prevProgressPoint); |
923 | 931 | } |
924 | 932 |
|
925 | | - return { lines, longestLineInColumns }; |
| 933 | + return { lines, longestLineInColumns, runData: runData.subarray(0, runDataLength) }; |
926 | 934 | } |
927 | 935 |
|
928 | 936 | async function createTextArea({ sourceIndex, text, progress, mappings, mappingsOffset, otherSource, originalName, bounds }) { |
929 | 937 | const shadowWidth = 16; |
930 | 938 | const textPaddingX = 5; |
931 | 939 | const textPaddingY = 1; |
932 | 940 | const scrollbarThickness = 16; |
933 | | - let { lines, longestLineInColumns } = await splitTextIntoLinesAndRuns(text, progress); |
| 941 | + |
| 942 | + // Runs are stored in a flat typed array to improve loading time |
| 943 | + const run_whitespace = index => runData[index] & 0xFF; |
| 944 | + const run_isSingleChunk = index => runData[index] & 0x100; |
| 945 | + const run_startIndex = index => runData[index + 1]; |
| 946 | + const run_endIndex = index => runData[index + 2]; |
| 947 | + const run_startColumn = index => runData[index + 3]; |
| 948 | + const run_endColumn = index => runData[index + 4]; |
| 949 | + |
| 950 | + let { lines, longestLineInColumns, runData } = await splitTextIntoLinesAndRuns(text, progress); |
934 | 951 | let animate = null; |
935 | 952 | let lastLineIndex = lines.length - 1; |
936 | 953 | let scrollX = 0; |
|
985 | 1002 | return { columnWidth, maxScrollX, maxScrollY, scrollbarX, scrollbarY }; |
986 | 1003 | } |
987 | 1004 |
|
988 | | - const emptyLine = { raw: '', runs: [] }; |
| 1005 | + const emptyLine = { raw: '', runCount: 0 }; |
989 | 1006 |
|
990 | 1007 | function analyzeLine(row, column, fractionalColumn, tabStopBehavior) { |
991 | 1008 | let index = column; |
992 | 1009 | let firstRun = 0; |
993 | 1010 | let nearbyRun = 0; |
994 | | - let { raw, runs, runsText } = row < lines.length ? lines[row] : emptyLine; |
995 | | - let runCount = runs.length; |
| 1011 | + let { raw, runBase, runCount, runText } = row < lines.length ? lines[row] : emptyLine; |
| 1012 | + let runLimit = runCount; |
996 | 1013 | let endOfLineIndex = 0; |
997 | 1014 | let endOfLineColumn = 0; |
998 | 1015 | let beforeNewlineIndex = 0; |
999 | 1016 | let hasTrailingNewline = false; |
1000 | 1017 |
|
1001 | | - if (runCount > 0) { |
1002 | | - let lastRun = runs[runCount - 1]; |
1003 | | - endOfLineIndex = lastRun.endIndex; |
1004 | | - endOfLineColumn = lastRun.endColumn; |
1005 | | - beforeNewlineIndex = lastRun.startIndex; |
1006 | | - hasTrailingNewline = (lastRun.whitespaceAndFlags & 0xFF) === 0x0A /* newline */; |
| 1018 | + if (runLimit > 0) { |
| 1019 | + let lastRun = runBase + 5 * (runLimit - 1); |
| 1020 | + endOfLineIndex = run_endIndex(lastRun); |
| 1021 | + endOfLineColumn = run_endColumn(lastRun); |
| 1022 | + beforeNewlineIndex = run_startIndex(lastRun); |
| 1023 | + hasTrailingNewline = run_whitespace(lastRun) === 0x0A /* newline */; |
1007 | 1024 |
|
1008 | 1025 | // Binary search to find the first run |
1009 | 1026 | firstRun = 0; |
1010 | | - while (runCount > 0) { |
1011 | | - let step = runCount >> 1; |
| 1027 | + while (runLimit > 0) { |
| 1028 | + let step = runLimit >> 1; |
1012 | 1029 | let it = firstRun + step; |
1013 | | - if (runs[it].endColumn < column) { |
| 1030 | + if (run_endColumn(runBase + 5 * it) < column) { |
1014 | 1031 | firstRun = it + 1; |
1015 | | - runCount -= step + 1; |
| 1032 | + runLimit -= step + 1; |
1016 | 1033 | } else { |
1017 | | - runCount = step; |
| 1034 | + runLimit = step; |
1018 | 1035 | } |
1019 | 1036 | } |
1020 | 1037 |
|
1021 | 1038 | // Use the last run if we're past the end of the line |
1022 | | - if (firstRun >= runs.length) firstRun--; |
| 1039 | + if (firstRun >= runCount) firstRun--; |
1023 | 1040 |
|
1024 | 1041 | // Convert column to index |
1025 | 1042 | nearbyRun = firstRun; |
1026 | | - while (runs[nearbyRun].startColumn > column && nearbyRun > 0) nearbyRun--; |
1027 | | - while (runs[nearbyRun].endColumn < column && nearbyRun + 1 < runs.length) nearbyRun++; |
1028 | | - let run = runs[nearbyRun]; |
1029 | | - if ((run.whitespaceAndFlags & 0x100 /* isSingleChunk */) && column <= run.endColumn) { |
| 1043 | + while (run_startColumn(runBase + 5 * nearbyRun) > column && nearbyRun > 0) nearbyRun--; |
| 1044 | + while (run_endColumn(runBase + 5 * nearbyRun) < column && nearbyRun + 1 < runCount) nearbyRun++; |
| 1045 | + let run = runBase + 5 * nearbyRun; |
| 1046 | + if (run_isSingleChunk(run) && column <= run_endColumn(run)) { |
1030 | 1047 | // A special case for single-character blocks such as tabs and emoji |
1031 | 1048 | if ( |
1032 | | - (tabStopBehavior === 'round' && fractionalColumn >= (run.startColumn + run.endColumn) / 2) || |
1033 | | - (tabStopBehavior === 'floor' && fractionalColumn >= run.endColumn) |
| 1049 | + (tabStopBehavior === 'round' && fractionalColumn >= (run_startColumn(run) + run_endColumn(run)) / 2) || |
| 1050 | + (tabStopBehavior === 'floor' && fractionalColumn >= run_endColumn(run)) |
1034 | 1051 | ) { |
1035 | | - index = run.endIndex; |
1036 | | - column = run.endColumn; |
| 1052 | + index = run_endIndex(run); |
| 1053 | + column = run_endColumn(run); |
1037 | 1054 | } else { |
1038 | | - index = run.startIndex; |
1039 | | - column = run.startColumn; |
| 1055 | + index = run_startIndex(run); |
| 1056 | + column = run_startColumn(run); |
1040 | 1057 | } |
1041 | 1058 | } else { |
1042 | | - index = run.startIndex + column - run.startColumn; |
| 1059 | + index = run_startIndex(run) + column - run_startColumn(run); |
1043 | 1060 | } |
1044 | 1061 | } |
1045 | 1062 |
|
|
1076 | 1093 | function columnToIndex(column) { |
1077 | 1094 | // If there is no underlying line, just use one index per column |
1078 | 1095 | let index = column; |
1079 | | - if (runs.length > 0) { |
1080 | | - while (runs[nearbyRun].startColumn > column && nearbyRun > 0) nearbyRun--; |
1081 | | - while (runs[nearbyRun].endColumn < column && nearbyRun + 1 < runs.length) nearbyRun++; |
1082 | | - let run = runs[nearbyRun]; |
1083 | | - index = column === run.endColumn ? run.endIndex : run.endIndex + column - run.startColumn; |
| 1096 | + if (runCount > 0) { |
| 1097 | + while (run_startColumn(runBase + 5 * nearbyRun) > column && nearbyRun > 0) nearbyRun--; |
| 1098 | + while (run_endColumn(runBase + 5 * nearbyRun) < column && nearbyRun + 1 < runCount) nearbyRun++; |
| 1099 | + let run = runBase + 5 * nearbyRun; |
| 1100 | + index = column === run_endColumn(run) ? run_endIndex(run) : run_endIndex(run) + column - run_startColumn(run); |
1084 | 1101 | } |
1085 | 1102 | return index; |
1086 | 1103 | } |
1087 | 1104 |
|
1088 | 1105 | function indexToColumn(index) { |
1089 | 1106 | // If there is no underlying line, just use one column per index |
1090 | 1107 | let column = index; |
1091 | | - if (runs.length > 0) { |
1092 | | - while (runs[nearbyRun].startIndex > index && nearbyRun > 0) nearbyRun--; |
1093 | | - while (runs[nearbyRun].endIndex < index && nearbyRun + 1 < runs.length) nearbyRun++; |
1094 | | - let run = runs[nearbyRun]; |
1095 | | - column = index === run.endIndex ? run.endColumn : run.startColumn + index - run.startIndex; |
| 1108 | + if (runCount > 0) { |
| 1109 | + while (run_startIndex(runBase + 5 * nearbyRun) > index && nearbyRun > 0) nearbyRun--; |
| 1110 | + while (run_endIndex(runBase + 5 * nearbyRun) < index && nearbyRun + 1 < runCount) nearbyRun++; |
| 1111 | + let run = runBase + 5 * nearbyRun; |
| 1112 | + column = index === run_endIndex(run) ? run_endColumn(run) : run_startColumn(run) + index - run_startIndex(run); |
1096 | 1113 | } |
1097 | 1114 | return column; |
1098 | 1115 | } |
|
1135 | 1152 | index, |
1136 | 1153 | column, |
1137 | 1154 | firstRun, |
1138 | | - runs, |
1139 | | - runsText, |
| 1155 | + runBase, |
| 1156 | + runCount, |
| 1157 | + runText, |
1140 | 1158 | firstMapping, |
1141 | 1159 | endOfLineIndex, |
1142 | 1160 | endOfLineColumn, |
|
1336 | 1354 | let dx = x - scrollX + margin + textPaddingX; |
1337 | 1355 | let dy = y - scrollY + textPaddingY; |
1338 | 1356 | dy += (row + 0.7) * rowHeight; |
1339 | | - const { raw, firstRun, runs, runsText, firstMapping, endOfLineColumn, rangeOfMapping, columnToIndex } = analyzeLine(row, firstColumn, firstColumn, 'floor'); |
| 1357 | + const { raw, firstRun, runBase, runCount, runText, firstMapping, endOfLineColumn, rangeOfMapping, columnToIndex } = analyzeLine(row, firstColumn, firstColumn, 'floor'); |
1340 | 1358 | const lastIndex = columnToIndex(lastColumn); |
1341 | 1359 |
|
1342 | 1360 | // Don't draw any text if the whole line is offscreen |
1343 | | - if (firstRun < runs.length) { |
| 1361 | + if (firstRun < runCount) { |
1344 | 1362 | // Scan to find the last run |
1345 | 1363 | let lastRun = firstRun; |
1346 | | - while (lastRun + 1 < runs.length && runs[lastRun + 1].startColumn < lastColumn) { |
| 1364 | + while (lastRun + 1 < runCount && run_startColumn(runBase + 5 * (lastRun + 1)) < lastColumn) { |
1347 | 1365 | lastRun++; |
1348 | 1366 | } |
1349 | 1367 |
|
1350 | 1368 | // Draw the runs |
1351 | 1369 | let currentColumn = firstColumn; |
1352 | 1370 | for (let i = firstRun; i <= lastRun; i++) { |
1353 | | - const run = runs[i]; |
1354 | | - let { whitespaceAndFlags, startColumn, endColumn } = run; |
1355 | | - let whitespace = whitespaceAndFlags & 0xFF; |
1356 | | - let text = runsText[i]; |
| 1371 | + const run = runBase + 5 * i; |
| 1372 | + let startColumn = run_startColumn(run); |
| 1373 | + let endColumn = run_endColumn(run); |
| 1374 | + let whitespace = run_whitespace(run); |
| 1375 | + let text = runText[i]; |
1357 | 1376 |
|
1358 | 1377 | // Lazily-generate text for runs to improve performance. When |
1359 | 1378 | // this happens, the run text is the code unit offset of the |
1360 | 1379 | // start of the line containing this run. |
1361 | 1380 | if (text === void 0) { |
1362 | | - text = runsText[i] = |
1363 | | - !whitespace ? raw.slice(run.startIndex, run.endIndex) : |
1364 | | - whitespace === 0x20 /* space */ ? '·'.repeat(run.endIndex - run.startIndex) : |
| 1381 | + text = runText[i] = |
| 1382 | + !whitespace ? raw.slice(run_startIndex(run), run_endIndex(run)) : |
| 1383 | + whitespace === 0x20 /* space */ ? '·'.repeat(run_endIndex(run) - run_startIndex(run)) : |
1365 | 1384 | whitespace === 0x0A /* newline */ ? row === lines.length - 1 ? '∅' : '↵' : |
1366 | 1385 | '→' /* tab */; |
1367 | 1386 | } |
1368 | 1387 |
|
1369 | 1388 | // Limit the run to the visible columns (but only for ASCII runs) |
1370 | | - if (!(run.whitespaceAndFlags & 0x100 /* isSingleChunk */)) { |
| 1389 | + if (!run_isSingleChunk(run)) { |
1371 | 1390 | if (startColumn < currentColumn) { |
1372 | 1391 | text = text.slice(currentColumn - startColumn); |
1373 | 1392 | startColumn = currentColumn; |
|
0 commit comments