I have recently tried to create a very simple VS Codium (VS Code) extension to support an embedded template language. In my case this was for Go templates (text/templates
/ html/templates
packages) which may be embedded in a variety of languages (such as HTML) with simple blocks: {{ go template syntax here }}
. The principle is the same as for CSS / JS / EJS or any other embeddable language. I'm not trying to do anything complex (no auto-completion or syntax checking), I just want 3 things to work:
- Syntax Highlighting (done)
- Hovers for ONLY template parts
- Snippets for ONLY template parts
For Go templates, an extension already exists (and works) for syntax highlighting, but as others have commented (and I can confirm), it runs all the time for a variety of file types (including HTML / JS), which makes the IDE slow down when dealing with larger projects (especially those that don't even need Go templates). I had actually been using another extension for my Go templates work, but this is for Hugo specifically, has several highlighting errors (trivial and reported but not fixed) and actually also always runs for all HTML / JS / JSON etc and adds its Go template snippets to all of these "polluting" other projects.
So... I made my own, simplified version of the former project to investigate why it is slow and I can see what (I think) needs to happen, but lack the experience in extension development to know where I should be looking to fix it.
To inject a language into another, existing language, the following package.json
statements are used:
package.json
(snipped)
{
"main": "./extension.js",
"activationEvents": [
"onLanguage:html"
],
"contributes": {
"languages": [
{
"id": "go-template",
"aliases": [
"Go Template",
"go-template"
],
"extensions": [
".go.tmpl",
".go.tpl",
".go.txt",
".gtmpl",
".gtpl"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "go-template",
"scopeName": "source.go-template",
"path": "./syntaxes/go-template.tmLanguage.json"
},
{
"scopeName": "text.injection.go-template",
"path": "./syntaxes/injection.go-template.tmLanguage.json",
"injectTo": [
"source.css",
"source.js",
"source.json",
"text.html.derivative",
"text.xml"
]
}
],
"snippets": [
{
"path": "./snippets/builtin.code-snippets"
}
]
}
Which crucially injects a simple go-template
language file into several others (though I'm only currently concerning myself with HTML). This language file looks like:
syntaxes/injection.go-template.tmLanguage.json
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"scopeName": "text.injection.go-template",
"name": "Go Template Injection",
"injectionSelector": "L:*",
"patterns": [
{
"include": "#templateTags"
}
],
"repository": {
"templateTags": {
"name": "meta.embedded.block.go-template",
"contentName": "source.go-template",
"begin": "\\{\\{-?[\\t ]*",
"beginCaptures": {
"0": {
"name": "punctuation.section.embedded.begin.go-template"
}
},
"end": "[\\t ]*-?\\}\\}",
"endCaptures": {
"0": {
"name": "punctuation.section.embedded.end.go-template"
},
"1": {
"name": "source.go-template"
}
},
"patterns": [
{
"include": "source.go-template"
}
]
}
}
}
I'll omit the actual language processing file (syntaxes/go-template.tmLanguage.json
), but this all works correctly and syntax highlighting is fine.
The issue arises when trying to add hovers for keyword / definition files, or snippets to work ONLY inside the go-template
areas.
If I run the extension in debug mode, and use the Scope and Element Inspector Tool on different parts of a page element I can see that HTML parts register as HTML, CSS parts register as CSS, JS parts register as JS, but Go Template parts still show as HTML? Surely there's enough information in the package.json
to map scope: source.go-template
-> language: go-template
?
For this reason I've currently bodged the ActivationEvents
to be "onLanguage:html"
(meaning that this will always run - even if no Go templates are present) rather than the desired: "onLanguage:go-template"
, and in the hovers file (extension.js
), I've had to do a scruffy check for only adding hovers to keywords directly following {{
which is slow and ugly.
extension.js
const vscode = require('vscode');
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
console.log('Go Template Support Activated...');
registerHoverProviders(context, ['html', 'javascript', 'css'])
}
function deactivate() { console.log('Go Template Support Deactivated...'); }
function registerHoverProviders(context, languages) {
languages.forEach(language => {
let hovers = vscode.languages.registerHoverProvider(language, {
provideHover(document, position, token) {
const range = document.getWordRangeAtPosition(position);
const word = document.getText(range);
if(word in definitions) {
const line = document.lineAt(position);
if(line.text.match(`{{.*` + word)) // Wouldn't need this (still not great)
{
const contents = new vscode.MarkdownString("");
contents.appendCodeblock(definitions[word]["definition"], "go");
contents.appendMarkdown(`\n____\n`);
contents.appendMarkdown(definitions[word]["explanation"]);
if("usage" in definitions[word]) {
contents.appendMarkdown(`\n____\nUsage:\n`);
contents.appendCodeblock(definitions[word]["usage"], "html");
}
contents.supportHtml = true;
contents.isTrusted = true;
return new vscode.Hover(contents);
}
}
}
});
context.subscriptions.push(hovers);
});
}
module.exports = {
activate,
deactivate
}
const definitions = {
// Go text/templates internal functions
"block": {
"definition": `func block(name string, vars any)`,
"explanation": `Defines a new template AND immediately renders it (akin to calling \`define\` and then \`template\`). May be over-ridden by a prior \`define\` block.`,
"usage": `{{ block "name" .RequiredVars }} contents {{ end }}`
}
}
These work:
BUT ideally should not need a loop of languages, instead only being attached to go-template
sections (i.e. active if, and only if, a Go template exists in the editor).
The issue continues into the snippets handling, which I've globalised so that I can specify what works in which scope. A conceptual example of what I'd like to have is:
snippets/builtin.code-snippets
{
"Go Template - Insert": {
"scope": "html, css, javascript",
"prefix": ["go"],
"body": ["{{- $0 -}}"],
"description": "Insert a Go Template into the current context"
},
"Go Template - Block": {
"scope": "go-template",
"prefix": ["block"],
"body": ["block \"${1:name}\" ${2:.} -}}", "\t$0", "{{- end"],
"description": "Insert a block statement into the Go Template block"
}
}
Where the first snippet would be the only global one, and all others only run for the language that they were intended for. Instead, the second one can never run in embedded files:
The docs seem to suggest that a language server is required for anything more complex, but the basic, forwarding and embedded examples are pretty overwhelming (for non-Node.js people) - the latter two literally re-implement HTML and CSS! I'm really not trying to do anything so complicated (error checking syntax / autocompletion etc), I just want to make hovers and snippets appear only for the correct language definitions so the editor doesn't slow down or show irrelevant suggestions.
Do I have any (simple) options?
EDIT: So I managed to get the scope to map to language within HTML using the embeddedLanguages
option in the grammars
section:
package.json
(snipped)
{
"scopeName": "text.injection.go-template",
"path": "./syntaxes/injection.go-template.tmLanguage.json",
"injectTo": [
"source.css",
"source.js",
"text.html.derivative",
"text.xml"
],
"embeddedLanguages": {
"source.go-template": "go-template"
}
}
but sadly this does not actually work at all either! It registers in the scope inspector tool, but nothing else actually changes. The only place that everything works (hovers / snippets) is in a file with an extension matching a value in the languages
.extensions
array.
EDIT 2: I realised that I needed the additional scope meta.embedded.block.go-template
mapped to the "go-template" language:
package.json
(snipped)
{
"scopeName": "text.injection.go-template",
"path": "./syntaxes/injection.go-template.tmLanguage.json",
"injectTo": [
"source.css",
"source.js",
"text.html.derivative",
"text.xml"
],
"embeddedLanguages": {
"source.go-template": "go-template"
"meta.embedded.block.go-template": "go-template"
}
}
and this actually makes the snippets work! It also makes changes the bracket colour to bright yellow(?), but I can live with that (or fix it later). Hovers sadly still do not work.
Any help would be appreciated. Thanks in advance.