on master help sign in
Create VS Code extension with syntax highlighting and LSP client integration
commita2d0f2df4bca57ec7d8b776c8f3ace7cb2cb156d authoriko <mail@iko.soy> dateTue 14 Apr 2026, 14:05 (6 months ago) parenta9313d2
8 files changed+408
editors/vscode/.gitignore+3 editors/vscode/.vscodeignore+2 editors/vscode/language-configuration.json+17 editors/vscode/package-lock.json+140 editors/vscode/package.json+59 editors/vscode/src/extension.ts+32 editors/vscode/syntaxes/htmlang.tmLanguage.json+142 editors/vscode/tsconfig.json+13
editors/vscode/.gitignorenew file+3
@@ -0,0 +1,3 @@
1+node_modules/
2+out/
3+*.vsix
editors/vscode/.vscodeignorenew file+2
@@ -0,0 +1,2 @@
1+src/**
2+tsconfig.json
editors/vscode/language-configuration.jsonnew file+17
@@ -0,0 +1,17 @@
1+{
2+ "comments": {
3+ "lineComment": "--"
4+ },
5+ "brackets": [
6+ ["[", "]"],
7+ ["{", "}"]
8+ ],
9+ "autoClosingPairs": [
10+ { "open": "[", "close": "]" },
11+ { "open": "{", "close": "}" }
12+ ],
13+ "surroundingPairs": [
14+ ["[", "]"],
15+ ["{", "}"]
16+ ]
17+}
editors/vscode/package-lock.jsonnew file+140
@@ -0,0 +1,140 @@
1+{
2+ "name": "htmlang",
3+ "version": "0.1.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "htmlang",
9+ "version": "0.1.0",
10+ "license": "MIT",
11+ "dependencies": {
12+ "vscode-languageclient": "^9.0.1"
13+ },
14+ "devDependencies": {
15+ "@types/node": "^20.0.0",
16+ "@types/vscode": "^1.75.0",
17+ "typescript": "^5.0.0"
18+ },
19+ "engines": {
20+ "vscode": "^1.75.0"
21+ }
22+ },
23+ "node_modules/@types/node": {
24+ "version": "20.19.39",
25+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
26+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
27+ "dev": true,
28+ "license": "MIT",
29+ "dependencies": {
30+ "undici-types": "~6.21.0"
31+ }
32+ },
33+ "node_modules/@types/vscode": {
34+ "version": "1.115.0",
35+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.115.0.tgz",
36+ "integrity": "sha512-/M8cdznOlqtMqduHKKlIF00v4eum4ZWKgn8YoPRKcN6PDdvoWeeqDaQSnw63ipDbq1Uzz78Wndk/d0uSPwORfA==",
37+ "dev": true,
38+ "license": "MIT"
39+ },
40+ "node_modules/balanced-match": {
41+ "version": "1.0.2",
42+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
43+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
44+ "license": "MIT"
45+ },
46+ "node_modules/brace-expansion": {
47+ "version": "2.1.0",
48+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
49+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
50+ "license": "MIT",
51+ "dependencies": {
52+ "balanced-match": "^1.0.0"
53+ }
54+ },
55+ "node_modules/minimatch": {
56+ "version": "5.1.9",
57+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
58+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
59+ "license": "ISC",
60+ "dependencies": {
61+ "brace-expansion": "^2.0.1"
62+ },
63+ "engines": {
64+ "node": ">=10"
65+ }
66+ },
67+ "node_modules/semver": {
68+ "version": "7.7.4",
69+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
70+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
71+ "license": "ISC",
72+ "bin": {
73+ "semver": "bin/semver.js"
74+ },
75+ "engines": {
76+ "node": ">=10"
77+ }
78+ },
79+ "node_modules/typescript": {
80+ "version": "5.9.3",
81+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
82+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
83+ "dev": true,
84+ "license": "Apache-2.0",
85+ "bin": {
86+ "tsc": "bin/tsc",
87+ "tsserver": "bin/tsserver"
88+ },
89+ "engines": {
90+ "node": ">=14.17"
91+ }
92+ },
93+ "node_modules/undici-types": {
94+ "version": "6.21.0",
95+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
96+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
97+ "dev": true,
98+ "license": "MIT"
99+ },
100+ "node_modules/vscode-jsonrpc": {
101+ "version": "8.2.0",
102+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
103+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
104+ "license": "MIT",
105+ "engines": {
106+ "node": ">=14.0.0"
107+ }
108+ },
109+ "node_modules/vscode-languageclient": {
110+ "version": "9.0.1",
111+ "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
112+ "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
113+ "license": "MIT",
114+ "dependencies": {
115+ "minimatch": "^5.1.0",
116+ "semver": "^7.3.7",
117+ "vscode-languageserver-protocol": "3.17.5"
118+ },
119+ "engines": {
120+ "vscode": "^1.82.0"
121+ }
122+ },
123+ "node_modules/vscode-languageserver-protocol": {
124+ "version": "3.17.5",
125+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
126+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
127+ "license": "MIT",
128+ "dependencies": {
129+ "vscode-jsonrpc": "8.2.0",
130+ "vscode-languageserver-types": "3.17.5"
131+ }
132+ },
133+ "node_modules/vscode-languageserver-types": {
134+ "version": "3.17.5",
135+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
136+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
137+ "license": "MIT"
138+ }
139+ }
140+}
editors/vscode/package.jsonnew file+59
@@ -0,0 +1,59 @@
1+{
2+ "name": "htmlang",
3+ "displayName": "htmlang",
4+ "description": "Language support for htmlang (.hl) files",
5+ "version": "0.1.0",
6+ "publisher": "htmlang",
7+ "license": "MIT",
8+ "engines": {
9+ "vscode": "^1.75.0"
10+ },
11+ "categories": [
12+ "Programming Languages"
13+ ],
14+ "activationEvents": [],
15+ "main": "./out/extension.js",
16+ "contributes": {
17+ "languages": [
18+ {
19+ "id": "htmlang",
20+ "aliases": [
21+ "htmlang"
22+ ],
23+ "extensions": [
24+ ".hl"
25+ ],
26+ "configuration": "./language-configuration.json"
27+ }
28+ ],
29+ "grammars": [
30+ {
31+ "language": "htmlang",
32+ "scopeName": "source.htmlang",
33+ "path": "./syntaxes/htmlang.tmLanguage.json"
34+ }
35+ ],
36+ "configuration": {
37+ "title": "htmlang",
38+ "properties": {
39+ "htmlang.server.path": {
40+ "type": "string",
41+ "default": "htmlang-lsp",
42+ "description": "Path to the htmlang-lsp binary"
43+ }
44+ }
45+ }
46+ },
47+ "dependencies": {
48+ "vscode-languageclient": "^9.0.1"
49+ },
50+ "devDependencies": {
51+ "@types/node": "^20.0.0",
52+ "@types/vscode": "^1.75.0",
53+ "typescript": "^5.0.0"
54+ },
55+ "scripts": {
56+ "build": "tsc",
57+ "watch": "tsc -w"
58+ }
59+}
editors/vscode/src/extension.tsnew file+32
@@ -0,0 +1,32 @@
1+import { ExtensionContext, workspace } from "vscode";
2+import {
3+ LanguageClient,
4+ LanguageClientOptions,
5+ ServerOptions,
6+} from "vscode-languageclient/node";
7+
8+let client: LanguageClient | undefined;
9+
10+export function activate(context: ExtensionContext) {
11+ const config = workspace.getConfiguration("htmlang");
12+ const command = config.get<string>("server.path", "htmlang-lsp");
13+
14+ const serverOptions: ServerOptions = { command };
15+
16+ const clientOptions: LanguageClientOptions = {
17+ documentSelector: [{ scheme: "file", language: "htmlang" }],
18+ };
19+
20+ client = new LanguageClient(
21+ "htmlang",
22+ "htmlang Language Server",
23+ serverOptions,
24+ clientOptions
25+ );
26+
27+ client.start();
28+}
29+
30+export function deactivate(): Thenable<void> | undefined {
31+ return client?.stop();
32+}
editors/vscode/syntaxes/htmlang.tmLanguage.jsonnew file+142
@@ -0,0 +1,142 @@
1+{
2+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3+ "name": "htmlang",
4+ "scopeName": "source.htmlang",
5+ "patterns": [
6+ { "include": "#comment" },
7+ { "include": "#raw-block" },
8+ { "include": "#page-directive" },
9+ { "include": "#let-definition" },
10+ { "include": "#define-definition" },
11+ { "include": "#fn-definition" },
12+ { "include": "#include-directive" },
13+ { "include": "#element" },
14+ { "include": "#function-call" },
15+ { "include": "#variable" },
16+ { "include": "#attributes" },
17+ { "include": "#inline-element" },
18+ { "include": "#color" },
19+ { "include": "#number" }
20+ ],
21+ "repository": {
22+ "comment": {
23+ "name": "comment.line.double-dash.htmlang",
24+ "match": "^\\s*--.*$"
25+ },
26+ "raw-block": {
27+ "name": "meta.raw.htmlang",
28+ "begin": "(@raw)\\s+(\"\"\")",
29+ "end": "(\"\"\")",
30+ "beginCaptures": {
31+ "1": { "name": "entity.name.tag.htmlang" },
32+ "2": { "name": "punctuation.definition.string.begin.htmlang" }
33+ },
34+ "endCaptures": {
35+ "1": { "name": "punctuation.definition.string.end.htmlang" }
36+ },
37+ "contentName": "string.unquoted.raw.htmlang"
38+ },
39+ "page-directive": {
40+ "match": "(@page)\\s+(.*)",
41+ "captures": {
42+ "1": { "name": "keyword.control.directive.htmlang" },
43+ "2": { "name": "string.unquoted.page-title.htmlang" }
44+ }
45+ },
46+ "let-definition": {
47+ "match": "(@let)\\s+([a-zA-Z_][a-zA-Z0-9_-]*)",
48+ "captures": {
49+ "1": { "name": "keyword.control.directive.htmlang" },
50+ "2": { "name": "variable.other.htmlang" }
51+ }
52+ },
53+ "define-definition": {
54+ "match": "(@define)\\s+([a-zA-Z_][a-zA-Z0-9_-]*)",
55+ "captures": {
56+ "1": { "name": "keyword.control.directive.htmlang" },
57+ "2": { "name": "variable.other.constant.htmlang" }
58+ }
59+ },
60+ "fn-definition": {
61+ "match": "(@fn)\\s+([a-zA-Z_][a-zA-Z0-9_-]*)",
62+ "captures": {
63+ "1": { "name": "keyword.control.directive.htmlang" },
64+ "2": { "name": "entity.name.function.definition.htmlang" }
65+ }
66+ },
67+ "include-directive": {
68+ "match": "(@include)\\s+(.*)",
69+ "captures": {
70+ "1": { "name": "keyword.control.directive.htmlang" },
71+ "2": { "name": "string.unquoted.filename.htmlang" }
72+ }
73+ },
74+ "element": {
75+ "name": "entity.name.tag.htmlang",
76+ "match": "@(?:row|column|col|el|text|paragraph|p|image|img|link|raw|children)\\b"
77+ },
78+ "function-call": {
79+ "name": "entity.name.function.htmlang",
80+ "match": "@[a-zA-Z_][a-zA-Z0-9_-]*"
81+ },
82+ "variable": {
83+ "name": "variable.other.htmlang",
84+ "match": "\\$[a-zA-Z_][a-zA-Z0-9_-]*"
85+ },
86+ "attributes": {
87+ "name": "meta.attribute-list.htmlang",
88+ "begin": "\\[",
89+ "end": "\\]",
90+ "beginCaptures": {
91+ "0": { "name": "punctuation.section.brackets.begin.htmlang" }
92+ },
93+ "endCaptures": {
94+ "0": { "name": "punctuation.section.brackets.end.htmlang" }
95+ },
96+ "patterns": [
97+ { "include": "#variable" },
98+ { "include": "#color" },
99+ { "include": "#number" },
100+ { "include": "#state-prefix" },
101+ { "include": "#attribute-name" },
102+ {
103+ "name": "punctuation.separator.comma.htmlang",
104+ "match": ","
105+ }
106+ ]
107+ },
108+ "state-prefix": {
109+ "name": "keyword.other.state.htmlang",
110+ "match": "(?:hover|active|focus):"
111+ },
112+ "attribute-name": {
113+ "name": "support.type.property-name.htmlang",
114+ "match": "\\b(?:spacing|padding|padding-x|padding-y|width|height|min-width|max-width|min-height|max-height|center-x|center-y|align-left|align-right|align-top|align-bottom|background|color|border|rounded|bold|italic|underline|size|font|transition|cursor|opacity|wrap|id|class)\\b"
115+ },
116+ "inline-element": {
117+ "name": "meta.inline-element.htmlang",
118+ "begin": "(\\{)(?=@)",
119+ "end": "(\\})",
120+ "beginCaptures": {
121+ "1": { "name": "punctuation.section.braces.begin.htmlang" }
122+ },
123+ "endCaptures": {
124+ "1": { "name": "punctuation.section.braces.end.htmlang" }
125+ },
126+ "patterns": [
127+ { "include": "#element" },
128+ { "include": "#function-call" },
129+ { "include": "#attributes" },
130+ { "include": "#variable" }
131+ ]
132+ },
133+ "color": {
134+ "name": "constant.other.color.htmlang",
135+ "match": "#[0-9a-fA-F]{3,8}\\b"
136+ },
137+ "number": {
138+ "name": "constant.numeric.htmlang",
139+ "match": "\\b[0-9]+(\\.[0-9]+)?\\b"
140+ }
141+ }
142+}
editors/vscode/tsconfig.jsonnew file+13
@@ -0,0 +1,13 @@
1+{
2+ "compilerOptions": {
3+ "module": "commonjs",
4+ "target": "es2020",
5+ "outDir": "out",
6+ "rootDir": "src",
7+ "strict": true,
8+ "sourceMap": true,
9+ "lib": ["es2020"]
10+ },
11+ "include": ["src"],
12+ "exclude": ["node_modules"]
13+}