Skip to content

Commit 2796019

Browse files
committed
Reduce startup time
1 parent 55cb1df commit 2796019

File tree

4 files changed

+68
-44
lines changed

4 files changed

+68
-44
lines changed

src/Highlighter.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class CodeHighlighter {
3838

3939
ec!: ExpressiveCodeEngine;
4040
ecElements!: HTMLElement[];
41-
loadedLanguages!: string[];
41+
supportedLanguages!: string[];
4242
shiki!: Highlighter;
4343
customThemes!: CustomTheme[];
4444
customLanguages!: LanguageRegistration[];
@@ -55,7 +55,10 @@ export class CodeHighlighter {
5555
await this.loadEC();
5656
await this.loadShiki();
5757

58-
this.loadedLanguages = this.shiki.getLoadedLanguages();
58+
this.supportedLanguages = [
59+
...Object.keys(bundledLanguages),
60+
...this.customLanguages.map(i => i.name)
61+
];
5962
}
6063

6164
async unload(): Promise<void> {
@@ -143,7 +146,7 @@ export class CodeHighlighter {
143146
themes: [new ExpressiveCodeTheme(await this.themeMapper.getThemeForEC())],
144147
plugins: [
145148
pluginShiki({
146-
langs: this.getLoadedLanguageRegistrations(),
149+
langs: [...this.customLanguages],
147150
}),
148151
pluginCollapsibleSections(),
149152
pluginTextMarkers(),
@@ -179,7 +182,7 @@ export class CodeHighlighter {
179182
async loadShiki(): Promise<void> {
180183
this.shiki = await createHighlighter({
181184
themes: [await this.themeMapper.getTheme()],
182-
langs: this.getLoadedLanguageRegistrations(),
185+
langs: [...this.customLanguages],
183186
});
184187
}
185188

@@ -198,7 +201,7 @@ export class CodeHighlighter {
198201
* All languages that are safe to use with Obsidian's `registerMarkdownCodeBlockProcessor`.
199202
*/
200203
obsidianSafeLanguageNames(): string[] {
201-
return this.loadedLanguages.filter(lang => !languageNameBlacklist.has(lang) && !this.plugin.loadedSettings.disabledLanguages.includes(lang));
204+
return this.supportedLanguages.filter(lang => !languageNameBlacklist.has(lang) && !this.plugin.loadedSettings.disabledLanguages.includes(lang));
202205
// .concat(this.customLanguages.map(lang => lang.name));
203206
}
204207

@@ -215,11 +218,16 @@ export class CodeHighlighter {
215218
container.innerHTML = toHtml(this.themeMapper.fixAST(result.renderedGroupAst));
216219
}
217220

218-
getHighlightTokens(code: string, lang: string): TokensResult | undefined {
221+
async getHighlightTokens(code: string, lang: string): Promise<TokensResult | undefined> {
219222
if (!this.obsidianSafeLanguageNames().includes(lang)) {
220223
return undefined;
221224
}
222-
225+
// load bundled language ​​when needed
226+
try {
227+
this.shiki.getLanguage(lang);
228+
} catch (e) {
229+
await this.shiki.loadLanguage(lang as BundledLanguage);
230+
}
223231
return this.shiki.codeToTokens(code, {
224232
lang: lang as BundledLanguage,
225233
theme: this.plugin.settings.theme,

src/codemirror/Cm6_ViewPlugin.ts

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import { Cm6_Util } from 'src/codemirror/Cm6_Util';
88
import { type ThemedToken } from 'shiki';
99
import { editorLivePreviewField } from 'obsidian';
1010

11+
interface DecoQueueNode {
12+
from: number;
13+
to: number;
14+
lang: string;
15+
content: string;
16+
hideLang?: boolean;
17+
hideTo?: number;
18+
}
19+
1120
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
1221
export function createCm6Plugin(plugin: ShikiPlugin) {
1322
return ViewPlugin.fromClass(
@@ -58,9 +67,10 @@ export function createCm6Plugin(plugin: ShikiPlugin) {
5867
* @param view
5968
* @param docChanged
6069
*/
61-
updateWidgets(view: EditorView, docChanged: boolean = true): void {
70+
async updateWidgets(view: EditorView, docChanged: boolean = true): Promise<void> {
6271
let lang = '';
6372
let state: SyntaxNode[] = [];
73+
let decoQueue: DecoQueueNode[] = [];
6474

6575
// const t1 = performance.now();
6676

@@ -82,26 +92,14 @@ export function createCm6Plugin(plugin: ShikiPlugin) {
8292
if (match) {
8393
const hasSelectionOverlap = Cm6_Util.checkSelectionAndRangeOverlap(view.state.selection, node.from - 1, node.to + 1);
8494

85-
// if there is selection overlap, the user has the inline code block selected, so we don't want to hide the language tag.
86-
// For this we just remove the decorations and rebuild them with the language tag visible.
87-
if (hasSelectionOverlap) {
88-
this.removeDecoration(node.from, node.to);
89-
}
90-
const hideTo = node.from + match[1].length + 3; // hide `{lang} `
91-
92-
try {
93-
const decorations = this.buildDecorations(hideTo, node.to, match[1], match[2]);
94-
95-
this.removeDecoration(node.from, node.to);
96-
// add the decoration that hides the language tag
97-
if (this.isLivePreview(view.state) && !hasSelectionOverlap) {
98-
decorations.unshift(Decoration.replace({}).range(node.from, hideTo));
99-
}
100-
// add the highlight decorations
101-
this.addDecoration(node.from, node.to, decorations);
102-
} catch (e) {
103-
console.error(e);
104-
}
95+
decoQueue.push({
96+
from: node.from,
97+
to: node.to,
98+
lang: match[1],
99+
content: match[2],
100+
hideLang: this.isLivePreview(view.state) && !hasSelectionOverlap,
101+
hideTo: node.from + match[1].length + 3 // hide `{lang} `
102+
});
105103
}
106104
} else {
107105
// we don't want to highlight normal inline code blocks, thus we remove any of our decorations
@@ -133,17 +131,12 @@ export function createCm6Plugin(plugin: ShikiPlugin) {
133131
const start = state[0].from;
134132
const end = state[state.length - 1].to;
135133

136-
const content = Cm6_Util.getContent(view.state, start, end);
137-
138-
try {
139-
const decorations = this.buildDecorations(start, end, lang, content);
140-
141-
// when we have the decorations, we first remove all existing decorations in the range and then add the new ones
142-
this.removeDecoration(start, end);
143-
this.addDecoration(start, end, decorations);
144-
} catch (e) {
145-
console.error(e);
146-
}
134+
decoQueue.push({
135+
from: start,
136+
to: end,
137+
lang,
138+
content: Cm6_Util.getContent(view.state, start, end)
139+
});
147140
}
148141

149142
lang = '';
@@ -152,6 +145,29 @@ export function createCm6Plugin(plugin: ShikiPlugin) {
152145
},
153146
});
154147

148+
for (const node of decoQueue) {
149+
try {
150+
if (node.hideTo) { // inline decorations
151+
const decorations = await this.buildDecorations(node.hideTo, node.to, node.lang, node.content);
152+
153+
this.removeDecoration(node.from, node.to);
154+
// add the decoration that hides the language tag
155+
if (node.hideLang) {
156+
decorations.unshift(Decoration.replace({}).range(node.from, node.hideTo));
157+
}
158+
// add the highlight decorations
159+
this.addDecoration(node.from, node.to, decorations);
160+
} else {
161+
const decorations = await this.buildDecorations(node.from, node.to, node.lang, node.content);
162+
163+
// when we have the decorations, we first remove all existing decorations in the range and then add the new ones
164+
this.removeDecoration(node.from, node.to);
165+
this.addDecoration(node.from, node.to, decorations);
166+
}
167+
} catch (e) {
168+
console.error(e);
169+
}
170+
}
155171
// console.log('Traversed syntax tree in', performance.now() - t1, 'ms');
156172
}
157173

@@ -201,12 +217,12 @@ export function createCm6Plugin(plugin: ShikiPlugin) {
201217
* @param language
202218
* @param content
203219
*/
204-
buildDecorations(from: number, to: number, language: string, content: string): Range<Decoration>[] {
220+
async buildDecorations(from: number, to: number, language: string, content: string): Promise<Range<Decoration>[]> {
205221
if (language === '') {
206222
return [];
207223
}
208224

209-
const highlight = plugin.highlighter.getHighlightTokens(content, language);
225+
const highlight = await plugin.highlighter.getHighlightTokens(content, language);
210226

211227
if (!highlight) {
212228
return [];

src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,12 @@ export default class ShikiPlugin extends Plugin {
115115
}
116116

117117
registerInlineCodeProcessor(): void {
118-
this.registerMarkdownPostProcessor((el, ctx) => {
118+
this.registerMarkdownPostProcessor(async (el, ctx) => {
119119
const inlineCodes = el.findAll(':not(pre) > code');
120120
for (let codeElm of inlineCodes) {
121121
let match = codeElm.textContent?.match(SHIKI_INLINE_REGEX); // format: `{lang} code`
122122
if (match) {
123-
const highlight = this.highlighter.getHighlightTokens(match[2], match[1]);
123+
const highlight = await this.highlighter.getHighlightTokens(match[2], match[1]);
124124
const tokens = highlight?.tokens.flat(1);
125125
if (!tokens?.length) {
126126
continue;

src/settings/SettingsTab.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export class ShikiSettingsTab extends PluginSettingTab {
135135
.setDesc('Configure language to exclude.')
136136
.addButton(button => {
137137
button.setButtonText('Add Language Rule').onClick(() => {
138-
const modal = new StringSelectModal(this.plugin, this.plugin.highlighter.loadedLanguages, language => {
138+
const modal = new StringSelectModal(this.plugin, this.plugin.highlighter.supportedLanguages, language => {
139139
this.plugin.settings.disabledLanguages.push(language);
140140
void this.plugin.saveSettings();
141141
this.display();

0 commit comments

Comments
 (0)