Skip to content

Commit a367b56

Browse files
authored
Improve show-whitespace performance on large files (#2737)
1 parent 30fab48 commit a367b56

2 files changed

Lines changed: 43 additions & 66 deletions

File tree

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
.rgh-ws-char {
1+
[data-rgh-whitespace] {
22
position: relative;
33
line-height: 1em;
44
text-indent: 0; /* Reset any indentation added by `indented-code-wrapping` feature */
55
}
66

7-
.rgh-ws-char::before {
7+
[data-rgh-whitespace]::before {
8+
content: attr(data-rgh-whitespace);
89
pointer-events: none;
910
user-select: none;
1011
position: absolute;
@@ -13,11 +14,6 @@
1314
opacity: 0.25;
1415
}
1516

16-
.rgh-tab-char::before {
17-
content: attr(data-rgh-tabs);
17+
[data-rgh-whitespace^='→']::before {
1818
letter-spacing: calc((var(--tab-size, 4) * 1ch) - 1ch);
1919
}
20-
21-
.rgh-space-char::before {
22-
content: attr(data-rgh-spaces);
23-
}

source/features/show-whitespace.tsx

Lines changed: 39 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,80 +6,61 @@ import onPrFileLoad from '../libs/on-pr-file-load';
66
import onNewComments from '../libs/on-new-comments';
77
import getTextNodes from '../libs/get-text-nodes';
88

9+
// `splitText` is used before and after each whitespace group so a new whitespace-only text node is created. This new node is then wrapped in a <span>
910
function showWhiteSpacesOn(line: Element): void {
10-
const textNodes = getTextNodes(line);
11+
for (const textNode of getTextNodes(line)) {
12+
// `textContent` reads must be cached #2737
13+
let text = textNode.textContent!;
1114

12-
for (const textNode of textNodes) {
13-
const textContent = textNode.textContent!;
14-
if (textContent.length === 0 || !(textContent.includes(' ') || textContent.includes('\t'))) {
15-
continue;
16-
}
15+
// Loop goes in reverse otherwise `splitText`'s `index` parameter needs to keep track of the previous split
16+
for (let i = text.length - 1; i >= 0; i--) {
17+
const thisCharacter = text[i];
1718

18-
const fragment = document.createDocumentFragment();
19+
// Exclude irrelevant characters
20+
if (thisCharacter !== ' ' && thisCharacter !== '\t') {
21+
continue;
22+
}
1923

20-
let lastEncounteredCharType;
21-
let charType: 'space' | 'tab' | 'other';
22-
let node;
24+
if (i < text.length - 1) {
25+
textNode.splitText(i + 1);
26+
}
2327

24-
for (const char of textContent) {
25-
if (char === ' ') {
26-
charType = 'space';
27-
} else if (char === '\t') {
28-
charType = 'tab';
29-
} else {
30-
charType = 'other';
28+
// Find the same character so they can be wrapped together
29+
while (text[i - 1] === thisCharacter) {
30+
i--;
3131
}
3232

33-
if (node && lastEncounteredCharType === charType) {
34-
node.textContent += char;
33+
textNode.splitText(i);
3534

36-
if (charType === 'space') {
37-
node.dataset.rghSpaces += '·';
38-
} else if (charType === 'tab') {
39-
node.dataset.rghTabs += '→';
40-
}
41-
} else {
42-
if (node) {
43-
fragment.append(node);
44-
}
35+
// Update cached variable here because it just changed
36+
text = textNode.textContent!;
4537

46-
if (charType === 'space') {
47-
node = <span className="rgh-ws-char rgh-space-char" data-rgh-spaces="·">{char}</span>;
48-
} else if (charType === 'tab') {
49-
node = <span className="rgh-ws-char rgh-tab-char" data-rgh-tabs="→">{char}</span>;
50-
} else {
51-
node = <>{char}</>;
52-
}
53-
}
38+
const whitespace = textNode.nextSibling!.textContent!
39+
.replace(/ /g, '·')
40+
.replace(/\t/g, '→');
5441

55-
lastEncounteredCharType = charType;
42+
textNode.after(
43+
<span data-rgh-whitespace={whitespace}>
44+
{textNode.nextSibling}
45+
</span>
46+
);
5647
}
48+
}
49+
}
5750

58-
if (node) {
59-
fragment.append(node);
51+
const viewportObserver = new IntersectionObserver(changes => {
52+
for (const change of changes) {
53+
if (change.isIntersecting) {
54+
showWhiteSpacesOn(change.target);
55+
viewportObserver.unobserve(change.target);
6056
}
61-
62-
textNode.replaceWith(fragment);
6357
}
64-
}
58+
});
6559

6660
async function run(): Promise<void> {
67-
const tables = select.all([
68-
'table.js-file-line-container:not(.rgh-showing-whitespace)', // Single blob file, and gist
69-
'.file table.diff-table:not(.rgh-showing-whitespace)', // Split and unified diffs
70-
'.file table.d-table:not(.rgh-showing-whitespace)' // "Suggested changes" in PRs
71-
].join());
72-
73-
for (const table of tables) {
74-
table.classList.add('rgh-showing-whitespace');
75-
76-
for (const [i, line] of select.all('.blob-code-inner', table).entries()) {
77-
showWhiteSpacesOn(line);
78-
79-
if (i % 100 === 0) {
80-
await new Promise(resolve => setTimeout(resolve)); // eslint-disable-line no-await-in-loop
81-
}
82-
}
61+
for (const line of select.all('.blob-code-inner:not(.rgh-observing-whitespace)')) {
62+
line.classList.add('rgh-observing-whitespace');
63+
viewportObserver.observe(line);
8364
}
8465
}
8566

0 commit comments

Comments
 (0)