Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sometimes the syntax does not get highlighted correctly. #17

Open
miketromba opened this issue Jan 6, 2021 · 2 comments
Open

Sometimes the syntax does not get highlighted correctly. #17

miketromba opened this issue Jan 6, 2021 · 2 comments

Comments

@miketromba
Copy link

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: #16

@zikaari
Copy link
Owner

zikaari commented Jan 12, 2021

can I see a larger code snippet?

@miketromba
Copy link
Author

miketromba commented Jan 13, 2021

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]
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants