mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(astro): add Astro language support and editor mode (#712)
Astro files get their own language: TypeScript frontmatter, HTML template with TSX expressions in text and tags, TypeScript scripts, astro-ls and oxlint servers, and file-type icons.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"src/bindings.ts",
|
||||
"src/editor/core/cm/angular/parser*.ts",
|
||||
"src/editor/core/cm/jsp/parser*.ts",
|
||||
"src/editor/core/cm/astro/parser*.ts",
|
||||
"docs/plugins/themes.md",
|
||||
"pnpm-lock.yaml",
|
||||
"**/*.toml",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"crates",
|
||||
"src/bindings.ts",
|
||||
"src/editor/core/cm/angular/parser*.ts",
|
||||
"src/editor/core/cm/astro/parser*.ts",
|
||||
"fixtures",
|
||||
"plugins",
|
||||
"test-results",
|
||||
|
||||
@@ -33,7 +33,8 @@ icon set, copied unmodified (some renamed) from JetBrains' open-source repositor
|
||||
- [intellij-plugins](https://github.com/JetBrains/intellij-plugins) (each plugin
|
||||
carries its own licence): `protobuf/protoeditor-core` (`newui/protoFile` →
|
||||
`fileTypes/protoFile`, Apache-2.0); `terraform` (`newui/hashiCorp` →
|
||||
`fileTypes/hashiCorp`, Apache-2.0); `makefile` (`expui/makefile` →
|
||||
`fileTypes/hashiCorp`, Apache-2.0); `Astro` (`icons/astro` → `fileTypes/astro`,
|
||||
Apache-2.0); `makefile` (`expui/makefile` →
|
||||
`fileTypes/makefile`, MIT, Copyright (c) 2016 Victor Kropp — full text below).
|
||||
|
||||
The read/write usage filter assets (`actions/showReadAccess{,_dark}.svg` and
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::sync::{Arc, OnceLock};
|
||||
/// Built-in definitions, embedded at compile time. Add new languages here.
|
||||
const BUILTIN: &[(&str, &str)] = &[
|
||||
("asciidoc", include_str!("../../../languages/asciidoc.toml")),
|
||||
("astro", include_str!("../../../languages/astro.toml")),
|
||||
("bat", include_str!("../../../languages/bat.toml")),
|
||||
("c", include_str!("../../../languages/c.toml")),
|
||||
("clojure", include_str!("../../../languages/clojure.toml")),
|
||||
@@ -602,6 +603,7 @@ mod tests {
|
||||
"Angular Template",
|
||||
"ERB",
|
||||
"JSP",
|
||||
"Astro",
|
||||
];
|
||||
|
||||
fn reg() -> LanguageRegistry {
|
||||
@@ -675,7 +677,7 @@ mod tests {
|
||||
let root = tmp.path();
|
||||
std::fs::create_dir_all(root.join("apps/web/src")).unwrap();
|
||||
std::fs::write(root.join("apps/web/.oxlintrc.json"), "{}").unwrap();
|
||||
for lang in ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "svelte"] {
|
||||
for lang in ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "svelte", "astro"] {
|
||||
let cfg = reg.get(&LanguageId::new(lang)).unwrap();
|
||||
let oxlint = cfg.language_servers.iter().find(|s| s.name == "oxlint").unwrap();
|
||||
assert_eq!((oxlint.command.as_str(), oxlint.args.as_slice()), ("oxlint", ["--lsp".to_string()].as_slice()));
|
||||
@@ -889,6 +891,7 @@ mod tests {
|
||||
("App.svelte", "svelte"),
|
||||
("index.jsp", "jsp"),
|
||||
("WEB-INF/tags/card.tag", "jsp"),
|
||||
("index.astro", "astro"),
|
||||
("package.json", "json"),
|
||||
("tsconfig.json", "jsonc"),
|
||||
("tsconfig.app.json", "jsonc"),
|
||||
@@ -950,6 +953,14 @@ mod tests {
|
||||
assert_eq!(res.line_comment.as_deref(), Some(";"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astro_server_reads_the_project_typescript() {
|
||||
let reg = reg();
|
||||
let ls = ®.get(&LanguageId::new("astro")).unwrap().language_servers[0];
|
||||
let tsdk = ls.initialization_options.as_ref().and_then(|o| o.get("typescript")?.get("tsdk")?.as_str());
|
||||
assert_eq!(tsdk, Some("node_modules/typescript/lib"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_shebangs() {
|
||||
let reg = reg();
|
||||
|
||||
@@ -7,6 +7,7 @@ const config: KnipConfig = {
|
||||
"src/**/*.{ts,tsx,css}",
|
||||
"!src/editor/core/cm/angular/parser.terms.ts",
|
||||
"!src/editor/core/cm/jsp/parser.terms.ts",
|
||||
"!src/editor/core/cm/astro/parser.terms.ts",
|
||||
"scripts/**/*.ts",
|
||||
"perf/**/*.ts",
|
||||
"e2e/**/*.ts",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
id = "astro"
|
||||
name = "Astro"
|
||||
extensions = ["astro"]
|
||||
block-comment = ["<!--", "-->"]
|
||||
indent = { tab-size = 2, insert-spaces = true }
|
||||
ui-mode = "Astro"
|
||||
|
||||
[[language-servers]]
|
||||
name = "astro-ls"
|
||||
command = "astro-ls"
|
||||
args = ["--stdio"]
|
||||
root-markers = ["astro.config.mjs", "astro.config.js", "astro.config.ts", "astro.config.mts", "astro.config.cjs", "astro.config.cts"]
|
||||
# astro-ls ships no TypeScript and refuses to start without `tsdk`. Servers start
|
||||
# in their workspace root, so this relative path is the project's own TypeScript
|
||||
# (6 or older: 7 has no tsserverlibrary.js). A TypeScript elsewhere, such as an
|
||||
# npm workspace root, goes in Settings › Language Servers › astro-ls ›
|
||||
# Initialization options as an absolute `typescript.tsdk`.
|
||||
initialization-options = { typescript = { tsdk = "node_modules/typescript/lib" } }
|
||||
install-hint = "npm i -g @astrojs/language-server, and add typescript@6 to the project"
|
||||
|
||||
[[language-servers]]
|
||||
name = "oxlint"
|
||||
command = "oxlint"
|
||||
args = ["--lsp"]
|
||||
root-markers = [".oxlintrc.json", ".oxlintrc.jsonc", "oxlint.config.ts", "oxlint.config.mts"]
|
||||
require-root-marker = true
|
||||
install-hint = "npm i -D oxlint"
|
||||
@@ -22,6 +22,7 @@
|
||||
"gen:windows-icons": "node scripts/gen-windows-icons.ts",
|
||||
"gen:angular-grammar": "lezer-generator src/editor/core/cm/angular/angular.grammar --typeScript -o src/editor/core/cm/angular/parser.ts",
|
||||
"gen:jsp-grammar": "lezer-generator src/editor/core/cm/jsp/jsp.grammar --typeScript -o src/editor/core/cm/jsp/parser.ts",
|
||||
"gen:astro-grammar": "lezer-generator src/editor/core/cm/astro/astro.grammar --typeScript -o src/editor/core/cm/astro/parser.ts",
|
||||
"gen:theme-docs": "node scripts/theme/gen-docs.ts",
|
||||
"theme:import-intellij": "node scripts/theme/import-intellij-theme.ts",
|
||||
"dev:app": "node scripts/tauri.ts dev --config src-tauri/tauri.dev.conf.json",
|
||||
|
||||
@@ -11,10 +11,10 @@ import { html } from "@codemirror/lang-html";
|
||||
import { javascriptLanguage } from "@codemirror/lang-javascript";
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
import { IterMode, parseMixed, type Input, type NestedParse, type SyntaxNode, type SyntaxNodeRef } from "@lezer/common";
|
||||
import { JS_TAGS } from "../jsTags";
|
||||
import { parser } from "./parser";
|
||||
|
||||
// @lezer/javascript styles `.` as a deref operator, but not `?.`.
|
||||
const jsParser = javascriptLanguage.parser.configure({ props: [styleTags({ "?.": t.derefOperator })] });
|
||||
const jsParser = javascriptLanguage.parser.configure({ props: [styleTags(JS_TAGS)] });
|
||||
const exprParser = jsParser.configure({ top: "SingleExpression" });
|
||||
|
||||
const baseParser = parser.configure({
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Astro components: the frontmatter, a TypeScript script between `---` fences,
|
||||
// then the template. `index.ts` parses the script as TypeScript and the
|
||||
// template as HTML with `{…}` expressions. Regenerate with
|
||||
// `pnpm gen:astro-grammar`.
|
||||
|
||||
@top Document { Frontmatter? Template? }
|
||||
|
||||
Frontmatter { FrontmatterFence FrontmatterScript? FrontmatterFence? }
|
||||
|
||||
@external tokens component from "./tokens" { FrontmatterFence, FrontmatterScript, Template }
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { indentUnit } from "@codemirror/language";
|
||||
import { buildParserFile } from "@lezer/generator";
|
||||
import { highlightTree, tagHighlighter, tags as t } from "@lezer/highlight";
|
||||
import { findLanguageDescription, loadSupport } from "../languages";
|
||||
import { pasteReindentFilter } from "../prefs";
|
||||
import { astro, astroLanguage } from "./index";
|
||||
|
||||
const highlighter = tagHighlighter([
|
||||
{ tag: t.processingInstruction, class: "fence" },
|
||||
{ tag: t.definitionKeyword, class: "def-kw" },
|
||||
{ tag: t.keyword, class: "keyword" },
|
||||
{ tag: t.typeName, class: "type" },
|
||||
{ tag: t.function(t.variableName), class: "call" },
|
||||
{ tag: t.variableName, class: "var" },
|
||||
{ tag: t.propertyName, class: "prop" },
|
||||
{ tag: t.attributeName, class: "attr" },
|
||||
{ tag: t.attributeValue, class: "value" },
|
||||
{ tag: t.string, class: "str" },
|
||||
{ tag: t.operator, class: "op" },
|
||||
{ tag: t.brace, class: "brace" },
|
||||
{ tag: t.tagName, class: "tag" },
|
||||
{ tag: t.comment, class: "comment" },
|
||||
{ tag: t.invalid, class: "invalid" },
|
||||
]);
|
||||
|
||||
/** `text:class` of the styled tokens of `doc`. */
|
||||
function tokens(doc: string): string[] {
|
||||
const out: string[] = [];
|
||||
highlightTree(astroLanguage.parser.parse(doc), highlighter, (from, to, cls) =>
|
||||
out.push(`${doc.slice(from, to)}:${cls}`),
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A layout with what made HTML mode misread Astro components. */
|
||||
const LAYOUT = `---
|
||||
import '@/styles/global.css'
|
||||
import Header from '@/components/Header.astro'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
visibility?: 'public' | 'private'
|
||||
}
|
||||
|
||||
const { title, visibility = 'private' } = Astro.props
|
||||
// The canonical URL drops the trailing slash, as <link rel="canonical"> expects.
|
||||
const pageUrl = new URL(Astro.url.pathname.replace(/(.)\\/$/u, '$1'), Astro.url).href
|
||||
const image = { path: '/og.png', width: 1200 }
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
{visibility === 'private' && <meta name="robots" content="noindex" />}
|
||||
{visibility === 'public' && (
|
||||
<>
|
||||
<link rel="canonical" href={pageUrl} />
|
||||
<meta property="og:image" content={new URL(image.path, Astro.url).href} />
|
||||
</>
|
||||
)}
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class="page">
|
||||
<Header {title} {...Astro.props} />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
::view-transition-old(root) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
const style = document.head.querySelector('style[data-emotion]')
|
||||
if (!style) {
|
||||
return
|
||||
}
|
||||
style.remove()
|
||||
})
|
||||
</script>
|
||||
`;
|
||||
|
||||
describe("Astro components", () => {
|
||||
it("parses the frontmatter as TypeScript, markup in its comments included", () => {
|
||||
const doc = "---\ninterface P { title: string }\nconst a = 'x' // on <html>\n---\n<p>{a}</p>";
|
||||
const out = tokens(doc);
|
||||
expect(out).toEqual(
|
||||
expect.arrayContaining([
|
||||
"---:fence",
|
||||
"interface:def-kw",
|
||||
"string:type",
|
||||
"const:def-kw",
|
||||
"'x':str",
|
||||
"// on <html>:comment",
|
||||
"p:tag",
|
||||
"a:var",
|
||||
]),
|
||||
);
|
||||
expect(out).not.toContain("html:tag");
|
||||
});
|
||||
|
||||
it("parses expressions in text, fragments and markup inside them included", () => {
|
||||
const out = tokens(
|
||||
"<head>\n {v === 'p' && <meta name=\"robots\" />}\n {ok && (\n <>\n <b>{n}</b>\n </>\n )}\n</head>",
|
||||
);
|
||||
expect(out).toEqual(
|
||||
expect.arrayContaining(["v:var", "===:op", "'p':str", "&&:op", "meta:tag", "name:attr", "n:var"]),
|
||||
);
|
||||
expect(out.filter((s) => s.endsWith(":invalid"))).toEqual([]);
|
||||
});
|
||||
|
||||
it("parses expressions in tags, with spaces, shorthands and spreads", () => {
|
||||
const out = tokens(
|
||||
'<meta content={new URL(img.path, Astro.url).href} title="{literal}" />\n<C {title} {...rest} />',
|
||||
);
|
||||
expect(out).toEqual(expect.arrayContaining(["content:attr", "new:keyword", "img:var", "title:var", "rest:var"]));
|
||||
expect(out).not.toContain("URL(img.path,:attr");
|
||||
expect(out).toContain('"{literal}":value');
|
||||
});
|
||||
|
||||
it("parses scripts as TypeScript and leaves styles to CSS", () => {
|
||||
const out = tokens("<script>\n const n: number = 1\n</script>\n<style>\n p { color: red; }\n</style>");
|
||||
expect(out).toEqual(expect.arrayContaining(["const:def-kw", "number:type", "script:tag", "style:tag"]));
|
||||
});
|
||||
|
||||
it("keeps a well-indented component unchanged when it is pasted", () => {
|
||||
// CodeMirror parses for at most 20 ms per transaction and indents with the partial tree.
|
||||
// A frozen clock lets a slow CI runner parse the whole paste.
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
try {
|
||||
const state = EditorState.create({
|
||||
extensions: [astro(), indentUnit.of(" "), EditorState.tabSize.of(2), pasteReindentFilter("indentEachLine")],
|
||||
});
|
||||
const pasted = state.update({ changes: { from: 0, insert: LAYOUT }, userEvent: "input.paste" }).state;
|
||||
expect(pasted.doc.toString()).toBe(LAYOUT);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("is generated from astro.grammar", () => {
|
||||
const dir = import.meta.dirname;
|
||||
const { parser, terms } = buildParserFile(readFileSync(resolve(dir, "astro.grammar"), "utf8"), {
|
||||
fileName: "src/editor/core/cm/astro/astro.grammar",
|
||||
moduleStyle: "es",
|
||||
typeScript: true,
|
||||
});
|
||||
expect(parser).toBe(readFileSync(resolve(dir, "parser.ts"), "utf8"));
|
||||
expect(terms).toBe(readFileSync(resolve(dir, "parser.terms.ts"), "utf8"));
|
||||
});
|
||||
|
||||
it("is the Astro mode", async () => {
|
||||
const desc = findLanguageDescription("Astro", "/site/src/pages/index.astro");
|
||||
expect((await loadSupport(desc!))?.language).toBe(astroLanguage);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Astro components: a TypeScript frontmatter between `---` fences
|
||||
* (`astro.grammar`), then an HTML template whose `{…}` expressions, in text
|
||||
* and in tags, are TSX. `<script>` is TypeScript, as Astro compiles it.
|
||||
*/
|
||||
import { LRLanguage, LanguageSupport } from "@codemirror/language";
|
||||
import { html } from "@codemirror/lang-html";
|
||||
import { tsxLanguage, typescriptLanguage } from "@codemirror/lang-javascript";
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
import { IterMode, parseMixed, type Input, type NestedParse, type SyntaxNodeRef } from "@lezer/common";
|
||||
import { JS_TAGS } from "../jsTags";
|
||||
import { parser } from "./parser";
|
||||
|
||||
const tsParser = typescriptLanguage.parser.configure({ props: [styleTags(JS_TAGS)] });
|
||||
const tsxParser = tsxLanguage.parser.configure({ props: [styleTags(JS_TAGS)] });
|
||||
|
||||
// To reuse old inner trees, parseMixed takes those that the node at a mount's
|
||||
// start and its ancestors had with the same parser. So each kind of mount has
|
||||
// its own instance (see angular/index.ts).
|
||||
const documentTextParser = tsxParser.configure({}),
|
||||
elementTextParser = tsxParser.configure({}),
|
||||
tagParser = tsxParser.configure({});
|
||||
|
||||
const templateHTML = html({
|
||||
selfClosingTags: true,
|
||||
// Other script types (`application/ld+json`) keep HTML's own nesting.
|
||||
nestedLanguages: [{ tag: "script", attrs: (a) => !a.type || a.type === "module", parser: tsParser }],
|
||||
});
|
||||
|
||||
const BRACE_L = 123,
|
||||
BRACE_R = 125,
|
||||
BACKSLASH = 92,
|
||||
QUOTES = new Set([34, 39, 96]);
|
||||
|
||||
type Range = { from: number; to: number };
|
||||
|
||||
/** Where a scan of expression braces stands between two pieces of text. */
|
||||
type Scan = { depth: number; quote: number };
|
||||
|
||||
function pushRange(out: Range[], from: number, to: number) {
|
||||
const last = out[out.length - 1];
|
||||
if (last && last.to === from) last.to = to;
|
||||
else out.push({ from, to });
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends to `out` the parts of `text`, which starts at `from`, inside `{…}`,
|
||||
* braces included. Strings count only inside braces, so prose apostrophes
|
||||
* stay text; in a tag (`attrQuotes`), quotes outside braces delimit attribute
|
||||
* values, whose braces are literal.
|
||||
*/
|
||||
function scanBraces(text: string, from: number, scan: Scan, out: Range[], attrQuotes = false) {
|
||||
let start = scan.depth > 0 ? 0 : -1;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text.charCodeAt(i);
|
||||
if (scan.quote) {
|
||||
if (ch === BACKSLASH && scan.depth > 0) i++;
|
||||
else if (ch === scan.quote) scan.quote = 0;
|
||||
} else if (QUOTES.has(ch) && (scan.depth > 0 || attrQuotes)) {
|
||||
scan.quote = ch;
|
||||
} else if (ch === BRACE_L) {
|
||||
if (scan.depth++ === 0) start = i;
|
||||
} else if (ch === BRACE_R && scan.depth > 0 && --scan.depth === 0) {
|
||||
pushRange(out, from + start, from + i + 1);
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
if (scan.depth > 0 && start >= 0) pushRange(out, from + start, from + text.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nodes of an element's own text. Markup inside an expression splits it into
|
||||
* several (`{ok && <b>yes</b>}`), and HTML reads `&&` as entities and the
|
||||
* fragment brackets `<>`, `</>` as incomplete tags.
|
||||
*/
|
||||
const TEXT_NODES = new Set([
|
||||
"Text",
|
||||
"EntityReference",
|
||||
"CharacterReference",
|
||||
"InvalidEntity",
|
||||
"IncompleteTag",
|
||||
"IncompleteCloseTag",
|
||||
]);
|
||||
|
||||
/** The expressions in the text directly in `node`, `null` without any. */
|
||||
function textExpressions(node: SyntaxNodeRef, input: Input): Range[] | null {
|
||||
const out: Range[] = [];
|
||||
const scan: Scan = { depth: 0, quote: 0 };
|
||||
const child = node.node.cursor(IterMode.IgnoreMounts);
|
||||
if (child.firstChild()) {
|
||||
do {
|
||||
if (TEXT_NODES.has(child.name)) scanBraces(input.read(child.from, child.to), child.from, scan, out);
|
||||
} while (child.nextSibling());
|
||||
}
|
||||
return out.length ? out : null;
|
||||
}
|
||||
|
||||
/** The expressions in a tag: values, shorthands and spreads, also with spaces, which split HTML attributes. */
|
||||
function tagExpressions(node: SyntaxNodeRef, input: Input): Range[] | null {
|
||||
const out: Range[] = [];
|
||||
scanBraces(input.read(node.from, node.to), node.from, { depth: 0, quote: 0 }, out, true);
|
||||
return out.length ? out : null;
|
||||
}
|
||||
|
||||
const TAGS = new Set(["OpenTag", "SelfClosingTag", "OpenScriptTag", "OpenStyleTag", "OpenTextareaTag"]);
|
||||
|
||||
function mixExpressions(node: SyntaxNodeRef, input: Input): NestedParse | null {
|
||||
if (TAGS.has(node.name)) {
|
||||
const overlay = tagExpressions(node, input);
|
||||
return overlay && { parser: tagParser, overlay };
|
||||
}
|
||||
if (node.name === "Document" || node.name === "Element") {
|
||||
const overlay = textExpressions(node, input);
|
||||
return overlay && { parser: node.name === "Document" ? documentTextParser : elementTextParser, overlay };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const templateParser = (templateHTML.language as LRLanguage).parser.configure({ wrap: parseMixed(mixExpressions) });
|
||||
|
||||
const componentParser = parser.configure({
|
||||
props: [styleTags({ FrontmatterFence: t.processingInstruction })],
|
||||
wrap: parseMixed((node) => {
|
||||
if (node.name === "FrontmatterScript") return { parser: tsParser };
|
||||
if (node.name === "Template") return { parser: templateParser };
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
|
||||
export const astroLanguage = LRLanguage.define({ name: "astro", parser: componentParser });
|
||||
|
||||
export function astro(): LanguageSupport {
|
||||
return new LanguageSupport(astroLanguage, [templateHTML.support]);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
FrontmatterFence = 1,
|
||||
FrontmatterScript = 2,
|
||||
Template = 3,
|
||||
Document = 4,
|
||||
Frontmatter = 5
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {component} from "./tokens"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "zQQOQOOOYORO'#CaQOOOOOQTOQOOOOOQ,58{,58{OhORO,58{OOOQ1G.g1G.g",
|
||||
stateData: "s~OPPORQO~OPSOQTORTXUTX~OPUORTaUTa~O",
|
||||
goto: "YUPPPPPVRRO",
|
||||
nodeNames: "⚠ FrontmatterFence FrontmatterScript Template Document Frontmatter",
|
||||
maxTerm: 6,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 0,
|
||||
tokenData: "RORO",
|
||||
tokenizers: [0, component],
|
||||
topRules: {"Document":[0,4]},
|
||||
tokenPrec: 0
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
/** External tokens of `astro.grammar`: the frontmatter fences, its script and the template. */
|
||||
import { ExternalTokenizer, type InputStream } from "@lezer/lr";
|
||||
import { FrontmatterFence, FrontmatterScript, Template } from "./parser.terms";
|
||||
|
||||
const NEWLINE = 10,
|
||||
CARRIAGE_RETURN = 13,
|
||||
SPACE = 32,
|
||||
TAB = 9,
|
||||
DASH = 45;
|
||||
|
||||
const isBlank = (ch: number) => ch === SPACE || ch === TAB;
|
||||
const isSpace = (ch: number) => isBlank(ch) || ch === NEWLINE || ch === CARRIAGE_RETURN;
|
||||
|
||||
/**
|
||||
* The end of a fence line starting at `offset` (a line holding `---` and
|
||||
* blanks, the Astro compiler allows both around it), or -1 when the line is
|
||||
* something else.
|
||||
*/
|
||||
function fenceEnd(input: InputStream, offset: number): number {
|
||||
let i = offset;
|
||||
while (isBlank(input.peek(i))) i++;
|
||||
if (input.peek(i) !== DASH || input.peek(i + 1) !== DASH || input.peek(i + 2) !== DASH) return -1;
|
||||
i += 3;
|
||||
while (isBlank(input.peek(i))) i++;
|
||||
const ch = input.peek(i);
|
||||
return ch === NEWLINE || ch === CARRIAGE_RETURN || ch === -1 ? i : -1;
|
||||
}
|
||||
|
||||
export const component = new ExternalTokenizer((input, stack) => {
|
||||
if (input.next === -1) return;
|
||||
const inFrontmatter = stack.canShift(FrontmatterScript);
|
||||
if (inFrontmatter || stack.canShift(FrontmatterFence)) {
|
||||
// The opening fence may follow blank lines at the start of the file.
|
||||
let start = 0;
|
||||
if (!inFrontmatter) while (isSpace(input.peek(start))) start++;
|
||||
const end = fenceEnd(input, start);
|
||||
if (end >= 0) {
|
||||
input.acceptToken(FrontmatterFence, end);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (inFrontmatter) {
|
||||
// The script runs to the start of the closing fence's line, or to the end.
|
||||
for (let i = 0; ; i++) {
|
||||
const ch = input.peek(i);
|
||||
if (ch === -1) return input.acceptToken(FrontmatterScript, i);
|
||||
if (ch === NEWLINE && fenceEnd(input, i + 1) >= 0) return input.acceptToken(FrontmatterScript, i + 1);
|
||||
}
|
||||
}
|
||||
let length = 0;
|
||||
while (input.peek(length) !== -1) length++;
|
||||
input.acceptToken(Template, length);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { tags as t, type styleTags } from "@lezer/highlight";
|
||||
|
||||
/** @lezer/javascript styles `.` as a deref operator, but not `?.`. */
|
||||
export const JS_TAGS: Parameters<typeof styleTags>[0] = { "?.": t.derefOperator };
|
||||
@@ -19,6 +19,7 @@ import { styleTags, tags as t } from "@lezer/highlight";
|
||||
import { languages as languageData } from "@codemirror/language-data";
|
||||
import type { DocBackend, LanguageInfo } from "../backend";
|
||||
import { _languageMode } from "@/editor/api";
|
||||
import { JS_TAGS } from "./jsTags";
|
||||
|
||||
let infos: Map<string, LanguageInfo> | null = null;
|
||||
let infosPromise: Promise<Map<string, LanguageInfo>> | null = null;
|
||||
@@ -120,6 +121,11 @@ const OWN_MODES = [
|
||||
load: () => import("./erb").then((m) => m.erb()),
|
||||
}),
|
||||
LanguageDescription.of({ name: "JSP", load: () => import("./jsp").then((m) => m.jsp()) }),
|
||||
LanguageDescription.of({
|
||||
name: "Astro",
|
||||
extensions: ["astro"],
|
||||
load: () => import("./astro").then((m) => m.astro()),
|
||||
}),
|
||||
LanguageDescription.of({
|
||||
name: "YAML",
|
||||
alias: ["yml"],
|
||||
@@ -252,9 +258,6 @@ const CSS_TAGS: Parameters<typeof styleTags>[0] = {
|
||||
"ClassName IdName PseudoClassName ColorLiteral CallTag": t.tagName,
|
||||
};
|
||||
|
||||
// @lezer/javascript styles `.` as a deref operator, but not `?.`.
|
||||
const JS_TAGS: Parameters<typeof styleTags>[0] = { "?.": t.derefOperator };
|
||||
|
||||
// @lezer/java tags annotations as plain identifiers.
|
||||
export const JAVA_TAGS: Parameters<typeof styleTags>[0] = {
|
||||
"MarkerAnnotation/Identifier Annotation/Identifier MarkerAnnotation Annotation": t.annotation,
|
||||
|
||||
@@ -40,7 +40,10 @@ function actionTitles(): Map<string, string> {
|
||||
for (const title of literals(node)) titles.set(title, `${source.fileName.slice(root.length)}:${line}`);
|
||||
};
|
||||
for (const source of program.getSourceFiles()) {
|
||||
if (!source.fileName.startsWith(`${root}src/`) || /\.test\.tsx?$|\/__tests__\/|\/test\//.test(source.fileName))
|
||||
if (
|
||||
!source.fileName.startsWith(`${root}src/`) ||
|
||||
/\.test\.tsx?$|\/__tests__\/|\/test\//.test(source.fileName.slice(root.length))
|
||||
)
|
||||
continue;
|
||||
const visit = (node: ts.Node) => {
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="16" height="16" viewBox="0 0 1280 1280" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M815.039 94.6439C824.758 106.709 829.714 122.99 839.626 155.553L1056.17 866.901C976.107 825.368 889.072 795.413 797.281 779.252L656.29 302.798C653.983 295.002 646.822 289.654 638.693 289.654C630.542 289.654 623.368 295.03 621.08 302.853L481.795 779.011C389.579 795.1 302.146 825.109 221.741 866.793L439.347 155.388L439.348 155.388C449.291 122.882 454.262 106.629 463.982 94.5853C472.562 83.9531 483.723 75.6958 496.4 70.6002C510.76 64.8284 527.756 64.8284 561.749 64.8284H717.174C751.212 64.8284 768.23 64.8284 782.603 70.6123C795.292 75.7184 806.459 83.9923 815.039 94.6439Z" fill="url(#paint0_linear_709_110)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M840.951 900.754C805.253 931.279 734.002 952.097 651.929 952.097C551.197 952.097 466.767 920.737 444.363 878.561C436.354 902.732 434.558 930.396 434.558 948.068C434.558 948.068 429.281 1034.84 489.636 1095.2C489.636 1063.86 515.042 1038.46 546.381 1038.46C600.097 1038.46 600.036 1085.32 599.987 1123.34C599.986 1124.48 599.984 1125.61 599.984 1126.73C599.984 1184.44 635.255 1233.91 685.416 1254.77C677.924 1239.36 673.721 1222.05 673.721 1203.77C673.721 1148.73 706.034 1128.23 743.588 1104.41L743.588 1104.41C773.469 1085.46 806.668 1064.41 829.548 1022.17C841.486 1000.13 848.265 974.893 848.265 948.068C848.265 931.573 845.702 915.676 840.951 900.754Z" fill="#FF5D01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M840.951 900.754C805.253 931.279 734.002 952.097 651.929 952.097C551.197 952.097 466.767 920.737 444.363 878.561C436.354 902.732 434.558 930.396 434.558 948.068C434.558 948.068 429.281 1034.84 489.636 1095.2C489.636 1063.86 515.042 1038.46 546.381 1038.46C600.097 1038.46 600.036 1085.32 599.987 1123.34C599.986 1124.48 599.984 1125.61 599.984 1126.73C599.984 1184.44 635.255 1233.91 685.416 1254.77C677.924 1239.36 673.721 1222.05 673.721 1203.77C673.721 1148.73 706.034 1128.23 743.588 1104.41L743.588 1104.41C773.469 1085.46 806.668 1064.41 829.548 1022.17C841.486 1000.13 848.265 974.893 848.265 948.068C848.265 931.573 845.702 915.676 840.951 900.754Z" fill="url(#paint1_linear_709_110)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_709_110" x1="882.997" y1="27.1132" x2="638.955" y2="866.902" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000014"/>
|
||||
<stop offset="1" stop-color="#150426"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_709_110" x1="1001.68" y1="652.45" x2="790.326" y2="1094.91" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF1639"/>
|
||||
<stop offset="1" stop-color="#FF1639" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg width="16" height="16" viewBox="0 0 1281 1280" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M815.931 94.6439C825.65 106.709 830.606 122.99 840.519 155.553L1057.06 866.901C976.999 825.368 889.964 795.413 798.174 779.252L657.182 302.798C654.875 295.002 647.715 289.654 639.585 289.654C631.434 289.654 624.26 295.03 621.972 302.853L482.688 779.011C390.471 795.1 303.038 825.109 222.634 866.793L440.24 155.388L440.24 155.388C450.183 122.882 455.154 106.629 464.874 94.5853C473.455 83.9531 484.616 75.6958 497.293 70.6002C511.652 64.8284 528.649 64.8284 562.642 64.8284H718.067C752.104 64.8284 769.123 64.8284 783.496 70.6123C796.184 75.7184 807.352 83.9923 815.931 94.6439Z" fill="url(#paint0_linear_709_106)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M841.843 900.754C806.146 931.279 734.895 952.097 652.822 952.097C552.089 952.097 467.659 920.737 445.256 878.561C437.247 902.732 435.45 930.396 435.45 948.068C435.45 948.068 430.173 1034.84 490.528 1095.2C490.528 1063.86 515.934 1038.46 547.273 1038.46C600.989 1038.46 600.929 1085.32 600.88 1123.34C600.878 1124.48 600.877 1125.61 600.877 1126.73C600.877 1184.44 636.147 1233.91 686.308 1254.77C678.816 1239.36 674.613 1222.05 674.613 1203.77C674.613 1148.73 706.926 1128.23 744.48 1104.41L744.481 1104.41C774.361 1085.46 807.56 1064.41 830.44 1022.17C842.379 1000.13 849.158 974.893 849.158 948.068C849.158 931.573 846.594 915.676 841.843 900.754Z" fill="#FF5D01"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M841.843 900.754C806.146 931.279 734.895 952.097 652.822 952.097C552.089 952.097 467.659 920.737 445.256 878.561C437.247 902.732 435.45 930.396 435.45 948.068C435.45 948.068 430.173 1034.84 490.528 1095.2C490.528 1063.86 515.934 1038.46 547.273 1038.46C600.989 1038.46 600.929 1085.32 600.88 1123.34C600.878 1124.48 600.877 1125.61 600.877 1126.73C600.877 1184.44 636.147 1233.91 686.308 1254.77C678.816 1239.36 674.613 1222.05 674.613 1203.77C674.613 1148.73 706.926 1128.23 744.48 1104.41L744.481 1104.41C774.361 1085.46 807.56 1064.41 830.44 1022.17C842.379 1000.13 849.158 974.893 849.158 948.068C849.158 931.573 846.594 915.676 841.843 900.754Z" fill="url(#paint1_linear_709_106)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_709_106" x1="883.889" y1="27.1132" x2="639.848" y2="866.902" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F9FAFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_709_106" x1="1002.57" y1="652.45" x2="791.219" y2="1094.91" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF1639"/>
|
||||
<stop offset="1" stop-color="#FF1639" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
+3
-1
@@ -78,6 +78,7 @@ const ICONS = {
|
||||
"file-vue": "fileTypes/vue",
|
||||
"file-svelte": "openidea/fileTypes/svelte",
|
||||
"file-jsp": "openidea/fileTypes/jsp",
|
||||
"file-astro": "fileTypes/astro",
|
||||
"file-yaml": "fileTypes/yaml",
|
||||
"file-toml": "fileTypes/toml",
|
||||
"file-html": "fileTypes/html",
|
||||
@@ -943,6 +944,7 @@ const EXT_ICONS = table({
|
||||
"file-vue": "vue",
|
||||
"file-svelte": "svelte",
|
||||
"file-jsp": "jsp jspx jspf jsph tag tagf tagx",
|
||||
"file-astro": "astro",
|
||||
"file-json": "json jsonc json5 jsonl ndjson geojson topojson webmanifest har avsc code-workspace code-snippets map",
|
||||
"file-md": "md mdx markdown mdown mkd mkdn mdwn ronn workbook",
|
||||
"file-rst": "rst",
|
||||
@@ -1026,7 +1028,7 @@ const EXT_ICONS = table({
|
||||
});
|
||||
|
||||
/** Source files for languages without a file-type icon. */
|
||||
const CODE_EXT = new Set(["astro", "nim", "wit", "wat", "sol", "v", "sv"]);
|
||||
const CODE_EXT = new Set(["nim", "wit", "wat", "sol", "v", "sv"]);
|
||||
|
||||
let iconOverride: ((fileName: string) => IconName | null) | null = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user