zikaari / monaco-editor-textmate

MIT License
122 stars 16 forks source link

Sometimes the syntax does not get highlighted correctly. #17

Open miketromba opened 3 years ago

miketromba commented 3 years ago

Here is a GIF of this where I am reloading the page... sometimes the syntax highlighting works, sometimes not.

monaco-textmate-highlighting-bug

In order to get the syntax highlighting to work 100% of the time, I had to make a minor adjustment to the example:

setTimeout(() => {
    wireTmGrammars(monaco, registry, grammars, editor)
}, 1)

I guess this pushes the wireTmGrammars behind whatever internal monaco operations it depends on. Note: I'm using monaco-editor version 0.21.2 with this fork of monaco-editor-textmate: https://github.com/NeekSandhu/monaco-editor-textmate/pull/16

zikaari commented 3 years ago

can I see a larger code snippet?

miketromba commented 3 years ago

Hey, I should have included more context. I've abstracted a lot into a separate class "MonacoTextmateManager" for my project; I'll share it here.

The basic structure is: MonacoTextmateEditor.vue is a vue component that imports the MonacoTextmateManager class and initializes it to display the editor.

At the bottom of the MonacoTextmateManager.spawnEditor method is this line which solves the problem I'm referencing:

setTimeout(this.wire.bind(this), 1)

Here is the source

MonacoTextmateEditor.vue - Vue component that displays the editor


<template>
<div id="MonacoEditor"></div>
</template>

<script>
import MonacoTextmateManager from '@/src/monaco/MonacoTextmateManager'
import oneDarkTextmateTheme from '@/src/monaco/one-dark-textmate-theme'

const ENABLED = false

export default {
    async mounted(){
        const monacoManager = new MonacoTextmateManager({
            languages: this.$config.languages,
            initialLanguage: 'javascript',
            pathToOnigWasm: '/monaco/onigasm.wasm'
        })
        // Spawn the editor
        if(ENABLED){
            await monacoManager.spawnEditor({
                element: document.getElementById('MonacoEditor'),
                theme: oneDarkTextmateTheme
            })
        }
    }
}
</script>

MonacoTextmateManager.js - class that manages the Monaco instance


import * as monaco from 'monaco-editor'

import { loadWASM } from 'onigasm' // peer dependency of 'monaco-textmate'
import { Registry } from 'monaco-textmate' // peer dependency
import { wireTmGrammars } from 'monaco-editor-textmate'

import ResourceCache from './ResourceCache'

export default class MonacoTextmateManager {
    constructor({
        languages = [],
        initialLanguage = 'javascript',
        pathToOnigWasm = '/monaco/onigasm.wasm',
        diffEditor = false
    }){
        this.languages = languages
        this.pathToOnigWasm = pathToOnigWasm
        this.diffEditor = diffEditor
        this.editor = null
        this.registry = null
        this.element = null
        this.language = this.languages.find(lang => lang.id == initialLanguage)
        this.cache = new ResourceCache({
            getters: {
                grammarFile: async grammarFileLocation => {
                    return (await fetch(grammarFileLocation)).text()
                },
                codeSample: async codeSampleLocation => {
                    return (await fetch(codeSampleLocation)).text()
                }
            }
        })
    }

    // Set theme data for the editor
    setTheme(themeData){
        // Monaco's built-in themes aren't powereful enough to handle TM tokens
        // https://github.com/Nishkalkashyap/monaco-vscode-textmate-theme-converter#monaco-vscode-textmate-theme-converter

        // Important, so that plaintext appears the correct color
        if(this.diffEditor){
            const baseTextColor = themeData.rules.find(rule => rule.token == 'source').foreground
            themeData = {
                ...themeData,
                rules: [
                    ...themeData.rules,
                    { token: '', foreground: baseTextColor }
                ]
            }
        }

        // Set the theme
        monaco.editor.defineTheme('vscode-theme-editor', themeData)
    }

    // Set the language of the editor
    async setLanguage(languageId){
        // Set this.language to new language object
        this.language = this.languages.find(lang => lang.id == languageId)

        // Inject the code sample for this language into the editor
        this.editor.setValue(
            await this.getCurrentCodeSample()
        )

        // Fully-respawn the editor
        return this.spawnEditor({ element: this.element })
    }

    // Spawn the editor instance
    async spawnEditor({ element, theme }){

        // Kill old editor if exists
        if(this.editor){ this.editor.dispose() }

        // Remember element
        this.element = element

        // Needed for monaco
        this.setWebWorkerPaths()

        // Define the theme
        if(theme) this.setTheme(theme)

        // Load WASM file for running onig regex in browser
        await this.loadWASMOnce()

        // Build the registry
        this.registry = new Registry({
            getGrammarDefinition: (async (scopeName) => {
                const grammarDefinition = await this.getCurrentGrammar()
                return {
                    format: 'json',
                    content: grammarDefinition
                }
            }).bind(this)
        })

        // Build the editor instance
        this.editor = monaco.editor[this.diffEditor? 'createDiffEditor' : 'create'](element, {
            value: await this.getCurrentCodeSample(),
            language: this.language.id,
            theme: 'vscode-theme-editor',
            automaticLayout: true,
            fontLigatures: true,
            links: true,
            minimap: {
                enabled: true
            },
            rulers: [60]
        })

        // Set models for diff editor preview
        if(this.diffEditor){
            this.editor.setModel({
                original: monaco.editor.createModel("This is what your diff editor will look like.\nYou should adjust these colors to fit nicely with your theme\nAnother line of text\nAnd another...", "text/plain"),
                modified: monaco.editor.createModel("just some text\n\nHello World\n\nSome more text", "text/plain")
            })
        }

        // Apply wireTMGrammars to the various objects
        // For some reason, we have to push this operation into the event stack for it to always work properly
        setTimeout(this.wire.bind(this), 1)
    }

    async wire(){
        return wireTmGrammars(monaco, this.registry, this.grammars, this.editor)
    }

    // Return a map of monaco "language id's" to TextMate scopeNames
    get grammars(){
        const grammars = new Map()
        this.languages.forEach(lang => grammars.set(lang.id, lang.scope))
        return grammars
    }

    // Returns the grammar file for the current language
    async getCurrentGrammar(){
        return this.cache.get('grammarFile', this.language.grammarFile)
    }

    // Returns the code sample for the current language
    async getCurrentCodeSample(){
        return this.cache.get('codeSample', this.language.codeSampleFile)
    }

    // Fetch and load the onig regex WASM file
    async loadWASMOnce(){
        if(window._ONIG_WASM_LOADED){ return }
        await loadWASM(this.pathToOnigWasm)
        window._ONIG_WASM_LOADED = true
    }
    setWebWorkerPaths(){
        // Since packaging is done by you, you need
        // to instruct the editor how you named the
        // bundles that contain the web workers.
        window.MonacoEnvironment = {
            getWorkerUrl: function (moduleId, label) {
                if (label === 'json') {
                    return '/monaco/workers/json.worker.js';
                }
                if (label === 'css' || label === 'scss' || label === 'less') {
                    return '/monaco/workers/css.worker.js';
                }
                if (label === 'html' || label === 'handlebars' || label === 'razor') {
                    return '/monaco/workers/html.worker.js';
                }
                if (label === 'typescript' || label === 'javascript') {
                    return '/monaco/workers/ts.worker.js';
                }
                return '/monaco/workers/editor.worker.js';
            }
        }
    }
}

ResourceCache.js - used above to acquire/cache resource like grammar files and code samples


export default class ResourceCache {
    constructor({ getters }){
        this.getters = getters
        this.resources = {}
        for(const getterId in this.getters){
            this.resources[getterId] = {}
        }
    }
    async get(getterId, resourceId){
        // Hydrate cache
        if(typeof this.resources[getterId][resourceId] == 'undefined'){       
            this.resources[getterId][resourceId] = this.getters[getterId](resourceId) // Set to a promise (this avoids concurrency bug)
        }
        // Return cached resource
        return this.resources[getterId][resourceId]
    }
}