Skip to content

Commit 08a04ba

Browse files
committed
feat: rainbow bracket pair colorizer
1 parent 77f05df commit 08a04ba

35 files changed

+432
-38
lines changed

src/ace/rainbowBracktes.js

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
const BATCH_SIZE = 500;
2+
3+
export default class AceRainbowBrackets {
4+
constructor() {
5+
this.bracketPairs = [
6+
{ open: "(", close: ")", type: "paren" },
7+
{ open: "[", close: "]", type: "bracket" },
8+
{ open: "{", close: "}", type: "curly" },
9+
];
10+
11+
this.colors = [
12+
"#ffd700", // Gold
13+
"#f472b6", // Pink
14+
"#57afff", // Blue
15+
"#7ce38b", // Green
16+
"#fb7185", // Red
17+
"#1edac1", // Teal
18+
];
19+
20+
this.ignoredTokenTypes = [
21+
"comment",
22+
"string",
23+
"regexp",
24+
"keyword",
25+
"doctype",
26+
"tag.doctype",
27+
"tag.comment",
28+
"text.xml",
29+
"string.regexp",
30+
"string.quoted",
31+
"string.single",
32+
"string.double",
33+
];
34+
35+
this.styleId = "ace-rainbow-brackets-style";
36+
this.visibleRows = null;
37+
this.disposeHandler = null;
38+
}
39+
40+
init(editor) {
41+
if (!editor) return null;
42+
43+
this.injectStyles();
44+
45+
this.isColorizationInProgress = false;
46+
this.pendingColorization = false;
47+
this.debounceTimeout = null;
48+
49+
const changeHandler = () => this.colorizeRainbowBrackets(editor);
50+
const scrollHandler = () => {
51+
this.updateVisibleRows(editor);
52+
this.colorizeRainbowBrackets(editor);
53+
};
54+
const afterRender = () => this.colorizeRainbowBrackets(editor);
55+
56+
editor.on("change", changeHandler);
57+
editor.session.on("changeScrollTop", scrollHandler);
58+
editor.renderer.on("afterRender", afterRender);
59+
60+
// Initial run
61+
this.updateVisibleRows(editor);
62+
this.colorizeRainbowBrackets(editor);
63+
64+
this.disposeHandler = {
65+
dispose: () => {
66+
editor.off("change", changeHandler);
67+
editor.session.off("changeScrollTop", scrollHandler);
68+
editor.renderer.off("afterRender", afterRender);
69+
this.clearTokenStyles(editor);
70+
editor.renderer.updateFull();
71+
this.removeStyles();
72+
},
73+
};
74+
75+
return this.disposeHandler;
76+
}
77+
78+
injectStyles() {
79+
const existingStyle = document.getElementById(this.styleId);
80+
if (existingStyle) {
81+
existingStyle.remove();
82+
}
83+
84+
const style = document.createElement("style");
85+
style.id = this.styleId;
86+
style.type = "text/css";
87+
88+
const css = this.colors
89+
.map((color, i) => `.rainbow-level-${i} { color: ${color} !important; }`)
90+
.join("\n");
91+
92+
style.textContent = css;
93+
document.head.appendChild(style);
94+
}
95+
96+
shouldIgnoreToken(tokenType) {
97+
if (!tokenType) return false;
98+
return this.ignoredTokenTypes.some((ignored) =>
99+
tokenType.includes(ignored),
100+
);
101+
}
102+
103+
removeStyles() {
104+
const style = document.getElementById(this.styleId);
105+
if (style) style.remove();
106+
}
107+
108+
colorizeRainbowBrackets(editor) {
109+
if (this.debounceTimeout) {
110+
clearTimeout(this.debounceTimeout);
111+
}
112+
113+
this.debounceTimeout = setTimeout(() => {
114+
if (this.isColorizationInProgress) {
115+
this.pendingColorization = true;
116+
return;
117+
}
118+
119+
this.isColorizationInProgress = true;
120+
this.pendingColorization = false;
121+
122+
// Process in chunks for large documents
123+
this.processColorizationInChunks(editor, () => {
124+
this.isColorizationInProgress = false;
125+
if (this.pendingColorization) {
126+
this.colorizeRainbowBrackets(editor);
127+
}
128+
});
129+
}, 100);
130+
}
131+
132+
processColorizationInChunks(editor, callback) {
133+
const session = editor.getSession();
134+
const doc = session.getDocument();
135+
const totalRows = doc.getLength();
136+
137+
this.clearTokenStyles(editor);
138+
139+
const bracketStack = [];
140+
141+
const openBracketMap = {};
142+
const closeBracketMap = {};
143+
this.bracketPairs.forEach((pair) => {
144+
openBracketMap[pair.open] = pair;
145+
closeBracketMap[pair.close] = pair;
146+
});
147+
148+
// Store bracket positions and info
149+
const allBracketsInfo = [];
150+
151+
// First pass: find all valid bracket pairs
152+
for (let row = 0; row < totalRows; row++) {
153+
const tokens = session.bgTokenizer?.lines[row] || [];
154+
155+
let columnOffset = 0;
156+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) {
157+
const token = tokens[tokenIndex];
158+
const tokenType = token.type || "";
159+
const tokenValue = token.value || "";
160+
161+
if (!this.shouldIgnoreToken(tokenType)) {
162+
for (let i = 0; i < tokenValue.length; i++) {
163+
const char = tokenValue[i];
164+
const column = columnOffset + i;
165+
166+
if (openBracketMap[char]) {
167+
const pairInfo = openBracketMap[char];
168+
bracketStack.push({
169+
row: row,
170+
column: column,
171+
tokenIndex: tokenIndex,
172+
char: char,
173+
pairType: pairInfo.type,
174+
closingChar: pairInfo.close,
175+
});
176+
} else if (closeBracketMap[char]) {
177+
const pairInfo = closeBracketMap[char];
178+
const matchingOpenChar = pairInfo.open;
179+
180+
let found = false;
181+
for (let j = bracketStack.length - 1; j >= 0; j--) {
182+
if (bracketStack[j].char === matchingOpenChar) {
183+
const openingBracket = bracketStack.splice(j, 1)[0];
184+
const level = j;
185+
186+
allBracketsInfo.push({
187+
row: openingBracket.row,
188+
column: openingBracket.column,
189+
tokenIndex: openingBracket.tokenIndex,
190+
char: openingBracket.char,
191+
level: level,
192+
pairType: openingBracket.pairType,
193+
});
194+
195+
allBracketsInfo.push({
196+
row: row,
197+
column: column,
198+
tokenIndex: tokenIndex,
199+
char: char,
200+
level: level,
201+
pairType: pairInfo.type,
202+
});
203+
204+
found = true;
205+
break;
206+
}
207+
}
208+
}
209+
}
210+
}
211+
212+
columnOffset += tokenValue.length;
213+
}
214+
}
215+
216+
this.processBatch(editor, allBracketsInfo, callback);
217+
}
218+
219+
processBatch(editor, allBracketsInfo, callback) {
220+
let currentIndex = 0;
221+
222+
const processBatchChunk = () => {
223+
const endIndex = Math.min(
224+
currentIndex + BATCH_SIZE,
225+
allBracketsInfo.length,
226+
);
227+
const batch = allBracketsInfo.slice(currentIndex, endIndex);
228+
229+
for (const info of batch) {
230+
this.applyColorToBracket(
231+
editor,
232+
info.row,
233+
info.column,
234+
info.level % this.colors.length,
235+
);
236+
}
237+
238+
currentIndex = endIndex;
239+
240+
if (currentIndex < allBracketsInfo.length) {
241+
setTimeout(processBatchChunk, 0);
242+
} else {
243+
editor.renderer.updateFull();
244+
if (callback) callback();
245+
}
246+
};
247+
248+
processBatchChunk();
249+
}
250+
251+
applyColorToBracket(editor, row, column, colorLevel) {
252+
const session = editor.getSession();
253+
const tokens = session.bgTokenizer?.lines[row];
254+
if (!tokens) return;
255+
256+
let tokenCol = 0;
257+
for (let i = 0; i < tokens.length; i++) {
258+
const token = tokens[i];
259+
const tokenEnd = tokenCol + token.value.length;
260+
261+
if (column >= tokenCol && column < tokenEnd) {
262+
if (this.shouldIgnoreToken(token.type)) {
263+
return;
264+
}
265+
266+
if (token.value.length === 1) {
267+
if (!token.type.includes(`rainbow-level-${colorLevel}`)) {
268+
token.type += ` rainbow-level-${colorLevel}`;
269+
}
270+
} else {
271+
const relativePos = column - tokenCol;
272+
const valueBefore = token.value.substring(0, relativePos);
273+
const bracketChar = token.value.charAt(relativePos);
274+
const valueAfter = token.value.substring(relativePos + 1);
275+
276+
const newTokens = [];
277+
278+
if (valueBefore) {
279+
newTokens.push({
280+
type: token.type,
281+
value: valueBefore,
282+
});
283+
}
284+
285+
newTokens.push({
286+
type: `${token.type} rainbow-level-${colorLevel}`,
287+
value: bracketChar,
288+
});
289+
290+
if (valueAfter) {
291+
newTokens.push({
292+
type: token.type,
293+
value: valueAfter,
294+
});
295+
}
296+
297+
tokens.splice(i, 1, ...newTokens);
298+
}
299+
300+
editor.renderer.updateLines(row, row);
301+
return;
302+
}
303+
304+
tokenCol = tokenEnd;
305+
}
306+
}
307+
308+
updateVisibleRows(editor) {
309+
const firstRow = editor.renderer.getFirstVisibleRow();
310+
const lastRow = editor.renderer.getLastVisibleRow();
311+
this.visibleRows = { first: firstRow, last: lastRow };
312+
}
313+
314+
clearTokenStyles(editor) {
315+
const session = editor.getSession();
316+
const doc = session.getDocument();
317+
318+
for (let row = 0; row < doc.getLength(); row++) {
319+
if (session.bgTokenizer?.lines[row]) {
320+
for (const token of session.bgTokenizer.lines[row]) {
321+
token.type = token.type.replace(/\s*rainbow-level-\d+/g, "");
322+
}
323+
editor.renderer.updateLines(row, row);
324+
}
325+
}
326+
}
327+
328+
dispose() {
329+
if (this.disposeHandler) {
330+
this.disposeHandler.dispose();
331+
this.disposeHandler = null;
332+
}
333+
}
334+
}

src/lang/ar-ye.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,6 @@
392392
"notifications": "Notifications",
393393
"no_unread_notifications": "No unread notifications",
394394
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
395-
"fade fold widgets": "Fade Fold Widgets"
395+
"fade fold widgets": "Fade Fold Widgets",
396+
"rainbowbrackets": "Rainbow Brackets Colorizer"
396397
}

src/lang/be-by.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,5 +393,6 @@
393393
"notifications": "Апавяшчэнні",
394394
"no_unread_notifications": "Няма непрачытаных апавяшчэнняў",
395395
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
396-
"fade fold widgets": "Fade Fold Widgets"
396+
"fade fold widgets": "Fade Fold Widgets",
397+
"rainbowbrackets": "Rainbow Brackets Colorizer"
397398
}

src/lang/bn-bd.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,6 @@
392392
"notifications": "Notifications",
393393
"no_unread_notifications": "No unread notifications",
394394
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
395-
"fade fold widgets": "Fade Fold Widgets"
395+
"fade fold widgets": "Fade Fold Widgets",
396+
"rainbowbrackets": "Rainbow Brackets Colorizer"
396397
}

src/lang/cs-cz.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,6 @@
392392
"notifications": "Notifications",
393393
"no_unread_notifications": "No unread notifications",
394394
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
395-
"fade fold widgets": "Fade Fold Widgets"
395+
"fade fold widgets": "Fade Fold Widgets",
396+
"rainbowbrackets": "Rainbow Brackets Colorizer"
396397
}

src/lang/de-de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,6 @@
392392
"notifications": "Benachrichtigungen",
393393
"no_unread_notifications": "Keine ungelesenen Benachrichtigungen",
394394
"should_use_current_file_for_preview": "Sollte die aktuelle Datei für die Vorschau verwenden, anstatt dem Standard (index.html)",
395-
"fade fold widgets": "Fade Fold Widgets"
395+
"fade fold widgets": "Fade Fold Widgets",
396+
"rainbowbrackets": "Rainbow Brackets Colorizer"
396397
}

src/lang/en-us.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,5 +393,6 @@
393393
"notifications": "Notifications",
394394
"no_unread_notifications": "No unread notifications",
395395
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
396-
"fade fold widgets": "Fade Fold Widgets"
396+
"fade fold widgets": "Fade Fold Widgets",
397+
"rainbowbrackets": "Rainbow Brackets Colorizer"
397398
}

src/lang/es-sv.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,6 @@
392392
"notifications": "Notifications",
393393
"no_unread_notifications": "No unread notifications",
394394
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
395-
"fade fold widgets": "Fade Fold Widgets"
395+
"fade fold widgets": "Fade Fold Widgets",
396+
"rainbowbrackets": "Rainbow Brackets Colorizer"
396397
}

src/lang/fr-fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,6 @@
392392
"notifications": "Notifications",
393393
"no_unread_notifications": "No unread notifications",
394394
"should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)",
395-
"fade fold widgets": "Fade Fold Widgets"
395+
"fade fold widgets": "Fade Fold Widgets",
396+
"rainbowbrackets": "Rainbow Brackets Colorizer"
396397
}

0 commit comments

Comments
 (0)