diff --git a/package.json b/package.json
index be6f3f58f..e35520a11 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,8 @@
"@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.5",
+ "@codemirror/lang-sass": "^6.0.2",
+ "@codemirror/lang-vue": "^0.1.3",
"@codemirror/language": "^6.13.1",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c8cb996b..accd1b17e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,12 @@ importers:
'@codemirror/lang-javascript':
specifier: ^6.2.5
version: 6.2.5
+ '@codemirror/lang-sass':
+ specifier: ^6.0.2
+ version: 6.0.2
+ '@codemirror/lang-vue':
+ specifier: ^0.1.3
+ version: 0.1.3
'@codemirror/language':
specifier: ^6.13.1
version: 6.13.1
diff --git a/src/editor/core/cm/languages.ts b/src/editor/core/cm/languages.ts
index 4c8d78ed9..54e66b58f 100644
--- a/src/editor/core/cm/languages.ts
+++ b/src/editor/core/cm/languages.ts
@@ -131,6 +131,12 @@ const OWN_MODES = [
extensions: ["astro"],
load: () => import("./astro").then((m) => m.astro()),
}),
+ // Overrides the language-data Vue mode so ``;
+ const tree = support!.language.parser.parse(src).toString();
+ // SCSS-only syntax that plain CSS can't parse: a nested rule and a $-variable.
+ expect(tree).toContain("SassVariableName");
+ // The nested .b ruleset only parses under the Sass grammar.
+ expect(tree).toMatch(/RuleSet\(ClassSelector.*RuleSet\(ClassSelector/s);
+ });
+
+ it("still parses a plain `)
+ .toString();
+ expect(tree).toContain("StyleSheet");
+ expect(tree).toContain("Declaration");
+ expect(tree).not.toContain("SassVariableName");
+ });
+});
diff --git a/src/editor/core/cm/vue.ts b/src/editor/core/cm/vue.ts
new file mode 100644
index 000000000..21d50c1f2
--- /dev/null
+++ b/src/editor/core/cm/vue.ts
@@ -0,0 +1,27 @@
+/**
+ * Vue single-file components: `@codemirror/lang-vue`'s parser over an HTML
+ * base that also highlights `