Skip to content

Commit 38895fc

Browse files
feat(monaco-editor): add shiki support for syntax highlighting (#2710)
Co-authored-by: Arash Ari Sheyda <38922203+arashsheyda@users.noreply.github.com>
1 parent ae1e662 commit 38895fc

File tree

9 files changed

+547
-376
lines changed

9 files changed

+547
-376
lines changed

packages/core/monaco-editor/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"@kong-ui-public/i18n": "workspace:^",
6767
"@kong/icons": "^1.41.0",
6868
"@kong/kongponents": ">=9.42.0 <10.0.0",
69-
"monaco-editor": ">=0.55.0 <1.0.0",
69+
"monaco-editor": ">=0.52.0 <1.0.0",
70+
"shiki": ">=3.0.0 <4.0.0",
7071
"vue": "^3.5.26"
7172
},
7273
"devDependencies": {
@@ -75,9 +76,12 @@
7576
"@kong/icons": "^1.41.0",
7677
"@kong/kongponents": "9.48.8",
7778
"monaco-editor": "^0.55.1",
79+
"shiki": "^3.20.0",
7880
"vue": "^3.5.26"
7981
},
8082
"dependencies": {
81-
"@vueuse/core": "^14.1.0"
83+
"@shikijs/monaco": "^3.20.0",
84+
"@vueuse/core": "^14.1.0",
85+
"shiki-codegen": "^3.20.0"
8286
}
8387
}

packages/core/monaco-editor/src/composables/useMonacoEditor.spec.ts

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@ import { describe, it, expect, vi } from 'vitest'
33
import { useMonacoEditor } from './useMonacoEditor'
44
import { mount } from '@vue/test-utils'
55

6+
const code = ref('initial code')
7+
8+
const dummyComponent = defineComponent({
9+
setup() {
10+
const target = ref<HTMLElement | null>(null)
11+
const editorApi = useMonacoEditor(target, {
12+
code,
13+
language: 'javascript',
14+
})
15+
return { target, editorApi }
16+
},
17+
template: '<div ref="target" />',
18+
})
19+
20+
621
describe('useMonacoEditor', () => {
722
beforeEach(() => {
823
vi.clearAllMocks()
@@ -12,29 +27,12 @@ describe('useMonacoEditor', () => {
1227
const el = document.createElement('div')
1328
document.body.appendChild(el)
1429

15-
const code = ref('initial code')
16-
17-
// define a simple dummy component using the composable
18-
const wrapper = mount(
19-
defineComponent({
20-
setup() {
21-
const target = ref<HTMLElement | null>(null)
22-
const editorApi = useMonacoEditor(target, {
23-
code,
24-
language: 'javascript',
25-
onChanged: vi.fn(),
26-
onCreated: vi.fn(),
27-
})
28-
return { target, editorApi }
29-
},
30-
template: '<div ref="target" />',
31-
}),
32-
)
30+
const wrapper = mount(dummyComponent)
3331

3432
// wait for next tick so lifecycle hooks run
3533
await nextTick()
3634

37-
const { editorApi } = wrapper.vm as any
35+
const { editorApi } = wrapper.vm
3836

3937
// ensure methods exist
4038
expect(editorApi).toHaveProperty('setContent')

packages/core/monaco-editor/src/composables/useMonacoEditor.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,50 @@
1-
import { onActivated, onBeforeUnmount, onMounted, reactive, toValue, watch } from 'vue'
1+
import { onActivated, onBeforeUnmount, onMounted, reactive, toValue, watch, ref } from 'vue'
22
import { DEFAULT_MONACO_OPTIONS } from '../constants'
33
import { unrefElement, useDebounceFn } from '@vueuse/core'
44

55
import * as monaco from 'monaco-editor'
6-
7-
import type * as monacoType from 'monaco-editor'
6+
import { shikiToMonaco } from '@shikijs/monaco'
7+
import { getSingletonHighlighter, bundledLanguages, bundledThemes } from 'shiki'
88

99
import type { MaybeComputedElementRef, MaybeElement } from '@vueuse/core'
1010
import type { MonacoEditorStates, UseMonacoEditorOptions } from '../types'
1111

12-
// singletons
13-
/** The Monaco instance once loaded */
14-
let monacoInstance: typeof monacoType | undefined = undefined
15-
16-
// cache
17-
const langCache = new Map<string, boolean>()
12+
// Flag if monaco loaded
13+
const isMonacoLoaded = ref(false)
14+
let initPromise: Promise<void> | null = null
1815

19-
/**
20-
* Lazily load Monaco and configure workers only once.
21-
*/
22-
function loadMonaco(language?: string): typeof monacoType {
23-
if (!monacoInstance) {
24-
monacoInstance = monaco
16+
async function loadMonaco() {
17+
if (initPromise) {
18+
return initPromise
2519
}
2620

27-
// TODO: register more languages as needed
28-
29-
// register language once
30-
if (language && !langCache.get(language)) {
31-
langCache.set(language, true)
32-
33-
if (!monaco.languages.getLanguages().some(lang => lang.id === language)) {
34-
monaco.languages.register({ id: language })
21+
initPromise = (async () => {
22+
try {
23+
// @ts-ignore jsonDefaults location varies across Monaco Editor versions
24+
// v0.55.0 introduced breaking changes and issues; Konnect still uses v0.52.x.
25+
const jsonDefaults = monaco.json?.jsonDefaults || monaco.languages.json?.jsonDefaults
26+
// Disable JSON token provider to prevent conflicts with @shikijs/monaco
27+
// https://github.com/shikijs/shiki/issues/865#issuecomment-3689158990
28+
jsonDefaults?.setModeConfiguration({ tokens: false })
29+
30+
const highlighter = await getSingletonHighlighter(
31+
{
32+
themes: Object.values(bundledThemes),
33+
langs: Object.values(bundledLanguages),
34+
},
35+
)
36+
highlighter.getLoadedLanguages().forEach(lang => {
37+
monaco.languages.register({ id: lang })
38+
})
39+
shikiToMonaco(highlighter, monaco)
40+
isMonacoLoaded.value = true
41+
} catch (error) {
42+
initPromise = null
43+
throw error
3544
}
36-
}
45+
})()
3746

38-
return monacoInstance
47+
return initPromise
3948
}
4049

4150
/**
@@ -53,7 +62,7 @@ export function useMonacoEditor<T extends MaybeElement>(
5362
* @type {monaco.editor.IStandaloneCodeEditor | undefined}
5463
* @default undefined
5564
*/
56-
let editor: monacoType.editor.IStandaloneCodeEditor | undefined
65+
let editor: monaco.editor.IStandaloneCodeEditor | undefined
5766

5867
// Internal flag to prevent multiple setups
5968
let _isSetup = false
@@ -112,34 +121,38 @@ export function useMonacoEditor<T extends MaybeElement>(
112121
}
113122

114123
/** Remeasure fonts in the editor with debouncing to optimize performance */
115-
const remeasureFonts = useDebounceFn(() => monacoInstance?.editor.remeasureFonts(), 200)
124+
const remeasureFonts = useDebounceFn(() => monaco.editor.remeasureFonts(), 200)
116125

117126

118127
const init = (): void => {
119-
const monaco = loadMonaco(options.language)
128+
loadMonaco()
120129

121-
// we want to create our model before creating the editor so we don't end up with multiple models for the same editor (v-if toggles, etc.)
122-
const uri = monaco.Uri.parse(`inmemory://model/${options.language}-${crypto.randomUUID()}`)
123-
const model = monaco.editor.createModel(options.code.value, options.language, uri)
130+
let model: monaco.editor.ITextModel | undefined
124131

125132
// `toValue()` safely unwraps refs, getters, or plain elements
126-
watch(() => toValue(target), (_target) => {
133+
watch([isMonacoLoaded, () => toValue(target)], ([_isLoaded, _target]) => {
127134

128135
// This ensures we skip setup if it's null, undefined, or an SVG element (as unrefElement can return SVGElement)
129136
const el = unrefElement(_target)
130-
if (!(el instanceof HTMLElement)) {
137+
if (!(el instanceof HTMLElement) || !_isLoaded) {
131138
_isSetup = false
132139
return
133140
}
134141

135142
// prevent multiple setups
136143
if (_isSetup) return
137144

145+
if (!model) {
146+
// we want to create our model before creating the editor so we don't end up with multiple models for the same editor (v-if toggles, etc.)
147+
const uri = monaco.Uri.parse(`inmemory://model/${options.language}-${crypto.randomUUID()}`)
148+
model = monaco.editor.createModel(options.code.value, options.language, uri)
149+
}
150+
138151
editor = monaco.editor.create(el, {
139152
...DEFAULT_MONACO_OPTIONS,
140153
readOnly: options.readOnly || false,
141154
language: options.language,
142-
theme: editorStates.theme,
155+
theme: editorStates.theme === 'light' ? 'catppuccin-latte' : 'catppuccin-mocha',
143156
model,
144157
editContext: false,
145158
...options.monacoOptions,

packages/core/monaco-editor/src/tests/setup.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,27 @@ vi.mock('monaco-editor', () => {
2424
})),
2525
})),
2626
remeasureFonts: vi.fn(),
27+
defineTheme: vi.fn(),
28+
setTheme: vi.fn(),
2729
createModel: vi.fn(() => ({})),
2830
}
2931

3032
const languages = {
3133
getLanguages: vi.fn(() => [{ id: 'javascript' }]),
3234
register: vi.fn(),
35+
setTokensProvider: vi.fn(),
36+
}
37+
38+
const json = {
39+
jsonDefaults: {
40+
setModeConfiguration: vi.fn(),
41+
},
3342
}
3443

3544
return {
3645
Uri,
3746
editor,
3847
languages,
48+
json,
3949
}
4050
})

packages/core/monaco-editor/vite-plugin/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ A plugin to simplify loading the [Monaco Editor](https://github.com/microsoft/mo
44

55
This plugin also configures and loads the Monaco Editor web workers for you, so you don't need to manually set them up as described in the [official Monaco Editor ESM integration guide](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md#using-vite).
66

7+
Since `@kong-public-ui/monaco-editor` uses [`@shikijs/monaco`](https://shiki.style/packages/monaco) to highlight Monaco Editor, this plugin also provides fine-grained control over the bundled languages and themes of shiki.
8+
9+
> [!IMPORTANT]
10+
> `@kong-public-ui/monaco-editor` loads all the languages and features of Monaco Editor and all the corresponding languages of shiki by default, which significantly increases bundle size. Furthermore, since `@shikijs/monaco` integrates Shiki with Monaco Editor synchronously and loads all languages by default, it greatly increases Monaco Editor's initial loading time. It is strongly recommended to use this Vite plugin to reduce bundle size and improve loading performance by selectively including only the languages and features you need.
11+
12+
> [!TIP]
13+
> This Vite plugin only needs to be applied in the consumer app. If you are developing a library based on Monaco Editor or `@kong-public-ui/monaco-editor`, you only need to externalize monaco-editor during the build.
14+
715
## Usage
816

917
```ts
@@ -25,6 +33,10 @@ Options can be passed in to `@kong-ui-public/monaco-editor/vite-plugin`. They ca
2533

2634
- `customLanguages` (`{label:string; entry:string; worker:{id:string, entry:string} }[]`) - Custom languages (outside of the ones shipped with the `monaco-editor`), e.g. [monaco-yaml](https://github.com/remcohaszing/monaco-yaml).
2735

36+
- `shiki` (`object`) - Shiki fine-grained bundle configuration:
37+
- `langs` (`string[]`) - Languages to include for Shiki syntax highlighting. By default, uses the same languages specified in the `languages` option above.
38+
- `themes` (`string[]`) - Themes to include for Shiki syntax highlighting. By default, `catppuccin-latte` and `catppuccin-mocha` are included.
39+
2840
## Example
2941

3042
```ts
@@ -53,6 +65,10 @@ export default defineConfig({
5365
},
5466
},
5567
],
68+
shiki: {
69+
langs: ['javascript', 'typescript', 'json', 'yaml'],
70+
themes: ['catppuccin-latte', 'catppuccin-mocha', 'nord'],
71+
},
5672
}),
5773
],
5874
})

0 commit comments

Comments
 (0)