Syntax Highlighting for Embedded Languages inside VS Code

1 minute read

Wrote a cool DSL and want to get syntax highlighting within a host language? Want to add syntax highlighting support for an existing language? This guide will walk through how to modify a VS code extension to do so.

In this example, a DSL for programming board games is embedded into typescript’s tag functions. When we’re done, it will look something like this:

Note: This guide will assume you have written a regular syntax-highlighting extension. VS Code has a guide on how to do this.

Note: The > Developer: Inspect Editor Tokens and Scopes command in VS Code is useful for debugging which scopes are assigned.

Creating the injection syntax

First, create a file like the following to find points within your host language’s source. In this example, begin is a regex which will find the start of a string template literal utilizing a tag function called ludi.

syntaxes/ts-injection.tmLanguage.json 
{
  "scopeName": "source.ts.embedded.ludi",
  // source.ts is the scope for our host language
  // `L` means to inject this grammar before `source.ts`
  // `-string -comment` means to exclude string and comment scopes
  "injectionSelector": "L:source.ts -string -comment",
  "patterns": [
    {
      "begin": "(?:ludi)\\s*`", // Look for strings like ludi`
      "end": "`", // Look for the ending `
      "contentName": "meta.embedded.block.ludi.typescript source.ludi",
      "patterns": [
        {
          // Process the ludi scope inside
          "include": "source.ludi"
        }
      ]
    }
  ]
}

Adding the injection to the plugin

Next, the grammar is added to package.json. In this example, support for entire ludi language already exists, so only the last section is added.

package.json 
{
  "name": "ludi",
  ...
  "contributes": {
    // Language configuration
    "languages": [{
      "id": "ludi",
      "aliases": ["Ludi", "ludi"],
      "extensions": [".ludi"],
      "configuration": "./language-configuration.json"
    }],
    "grammars": [
      // Grammar for Ludi
      {
        "language": "ludi",
        "scopeName": "source.ludi",
        "path": "./syntaxes/ludi.tmLanguage.json"
      },

      // Injecting into typescript's grammar. This part is new.
      {
        "path": "./syntaxes/ts-injection.tmLanguage.json",
        "scopeName": "source.ts.embedded.ludi", // Scope matches `scopeName` in the grammar
        "injectTo": ["source.ts"], // Host language's scope
        "embeddedLanguages": {
          "meta.embedded.inline.ludi.typescript": "ludi" // The language id defined above
        }
      }
    ]
  }
}

That’s it, you’re done!