diff --git a/crates/ide-language/src/registry.rs b/crates/ide-language/src/registry.rs index 31a9eda2..908d3ca5 100644 --- a/crates/ide-language/src/registry.rs +++ b/crates/ide-language/src/registry.rs @@ -8,6 +8,7 @@ use std::sync::Arc; /// Built-in definitions, embedded at compile time. Add new languages here. const BUILTIN: &[(&str, &str)] = &[ + ("asciidoc", include_str!("../../../languages/asciidoc.toml")), ("bat", include_str!("../../../languages/bat.toml")), ("c", include_str!("../../../languages/c.toml")), ("clojure", include_str!("../../../languages/clojure.toml")), @@ -373,6 +374,7 @@ mod tests { /// `name`s exported by `@codemirror/language-data` or built into `src/editor/core/cm/languages.ts` /// (the UI resolves `ui-mode` against these). const UI_MODES: &[&str] = &[ + "AsciiDoc", "Batch", "C", "C++", @@ -532,6 +534,12 @@ mod tests { let reg = reg(); assert_eq!(detect(®, "src/main.rs"), "rust"); assert_eq!(detect(®, "a/b.TS"), "typescript"); + for path in ["guide.adoc", "guide.asciidoc", "GUIDE.ADOC"] { + assert_eq!(detect(®, path), "asciidoc"); + } + let asciidoc = reg.get(&LanguageId::new("asciidoc")).unwrap(); + assert_eq!(asciidoc.ui_mode.as_deref(), Some("AsciiDoc")); + assert_eq!(asciidoc.line_comment.as_deref(), Some("//")); assert_eq!(reg.detect(Path::new("script"), Some("#!/usr/bin/env python3")).as_str(), "python"); assert_eq!(detect(®, "README"), "plaintext"); assert_eq!(detect(®, "unknown.zzz"), "plaintext"); diff --git a/languages/asciidoc.toml b/languages/asciidoc.toml new file mode 100644 index 00000000..e798cbbd --- /dev/null +++ b/languages/asciidoc.toml @@ -0,0 +1,6 @@ +id = "asciidoc" +name = "AsciiDoc" +extensions = ["adoc", "asciidoc"] +line-comment = "//" +indent = { tab-size = 2, insert-spaces = true } +ui-mode = "AsciiDoc" diff --git a/src/editor/core/backend.ts b/src/editor/core/backend.ts index 8e4b020c..55dfc59a 100644 --- a/src/editor/core/backend.ts +++ b/src/editor/core/backend.ts @@ -119,6 +119,8 @@ const EXT_LANG: Record = { go: "go", json: "json", md: "markdown", + adoc: "asciidoc", + asciidoc: "asciidoc", css: "css", html: "html", yml: "yaml", @@ -218,6 +220,15 @@ const MEM_LANGUAGES: LanguageInfo[] = [ blockComment: null, indent: { tabSize: 2, insertSpaces: true }, }, + { + id: "asciidoc", + name: "AsciiDoc", + extensions: ["adoc", "asciidoc"], + uiMode: "AsciiDoc", + lineComment: "//", + blockComment: null, + indent: { tabSize: 2, insertSpaces: true }, + }, { id: "markdown", name: "Markdown", diff --git a/src/editor/core/cm/asciidoc.ts b/src/editor/core/cm/asciidoc.ts new file mode 100644 index 00000000..b24a5f3a --- /dev/null +++ b/src/editor/core/cm/asciidoc.ts @@ -0,0 +1,50 @@ +import { StreamLanguage, type StreamParser } from "@codemirror/language"; + +type State = { block: string | null }; + +const parser: StreamParser = { + startState: () => ({ block: null }), + copyState: ({ block }) => ({ block }), + token(stream, state) { + if (stream.sol()) { + const line = stream.string; + const delimiter = line.trim(); + if (state.block) { + if (delimiter === state.block) { + state.block = null; + stream.skipToEnd(); + return "meta"; + } + stream.skipToEnd(); + return "monospace"; + } + if (/^={1,6}\s+\S/.test(line)) { + stream.skipToEnd(); + return "heading"; + } + if (/^:[\w-]+:/.test(line)) { + stream.skipToEnd(); + return "attributeName"; + } + if (["----", "....", "____", "++++"].includes(delimiter)) { + state.block = delimiter; + stream.skipToEnd(); + return "meta"; + } + if (/^\s*\/\//.test(line)) { + stream.skipToEnd(); + return "comment"; + } + } + if (stream.eatSpace()) return null; + if (stream.match(/^https?:\/\/[^\s[\]<>]+/)) return "link"; + if (stream.match(/^\*[^*\n]+\*/)) return "strong"; + if (stream.match(/^_[^_\n]+_/)) return "emphasis"; + if (stream.match(/^\x60[^\x60\n]+\x60/)) return "monospace"; + stream.next(); + return null; + }, + languageData: { commentTokens: { line: "//" } }, +}; + +export const asciidocLanguage = StreamLanguage.define(parser); diff --git a/src/editor/core/cm/languages.test.ts b/src/editor/core/cm/languages.test.ts index 26101398..72d674ea 100644 --- a/src/editor/core/cm/languages.test.ts +++ b/src/editor/core/cm/languages.test.ts @@ -3,6 +3,54 @@ import { highlightTree, tagHighlighter, tags as t } from "@lezer/highlight"; import { findLanguageDescription, loadSupport } from "./languages"; describe("language modes", () => { + it("recognizes AsciiDoc files and loads their lightweight syntax mode", async () => { + for (const path of ["/docs/guide.adoc", "/docs/guide.asciidoc"]) { + const description = findLanguageDescription("AsciiDoc", path); + expect(description?.name).toBe("AsciiDoc"); + const support = await loadSupport(description!); + expect(support?.language).toBeTruthy(); + } + }); + + it("assigns highlight tags to AsciiDoc syntax", async () => { + const support = await loadSupport(findLanguageDescription("AsciiDoc", "/docs/guide.adoc")!); + const doc = [ + "= Guide", + ":toc: left", + "----", + "const example = true;", + "----", + "*bold* _italic_ `code` https://example.com", + "// Comment", + ].join("\n"); + const highlighter = tagHighlighter([ + { tag: t.heading, class: "heading" }, + { tag: t.attributeName, class: "attribute" }, + { tag: t.meta, class: "meta" }, + { tag: t.monospace, class: "monospace" }, + { tag: t.strong, class: "strong" }, + { tag: t.emphasis, class: "emphasis" }, + { tag: t.link, class: "link" }, + { tag: t.comment, class: "comment" }, + ]); + const highlighted: [string, string][] = []; + highlightTree(support!.language.parser.parse(doc), highlighter, (from, to, style) => { + highlighted.push([doc.slice(from, to), style]); + }); + expect(highlighted).toEqual([ + ["= Guide", "heading"], + [":toc: left", "attribute"], + ["----", "meta"], + ["const example = true;", "monospace"], + ["----", "meta"], + ["*bold*", "strong"], + ["_italic_", "emphasis"], + ["`code`", "monospace"], + ["https://example.com", "link"], + ["// Comment", "comment"], + ]); + }); + it("styles `?.` like `.` in JavaScript and TypeScript", async () => { const highlighter = tagHighlighter([{ tag: t.derefOperator, class: "deref" }]); for (const [mode, path] of [ diff --git a/src/editor/core/cm/languages.ts b/src/editor/core/cm/languages.ts index bcd1d9bd..9be9fbe3 100644 --- a/src/editor/core/cm/languages.ts +++ b/src/editor/core/cm/languages.ts @@ -68,6 +68,11 @@ const OWN_MODES = [ filename: /^(?:CMakeLists\.txt|.*\.cmake(?:\.in)?)$/, load: async () => new LanguageSupport((await import("./cmake")).cmakeLanguage), }), + LanguageDescription.of({ + name: "AsciiDoc", + extensions: ["adoc", "asciidoc"], + load: () => import("./asciidoc").then((m) => new LanguageSupport(m.asciidocLanguage)), + }), LanguageDescription.of({ name: "Shell", load: async () => { @@ -245,7 +250,7 @@ const DEDENT_STATEMENT = /^\s*(?:pass|break|continue|return)\b/; const KEYWORD_OPEN_LANGS = new Set(["elixir", "ruby", "lua"]); const KEYWORD_OPEN = /\b(?:do(?:\s*\|[^|]*\|)?|then|->)$|^\s*(?:def|defp|defmodule|class|module|function|local\s+function|if|unless|elsif|elseif|else|while|until|for|case|begin|cond|with|receive|try)\b/; -const NO_INDENT_LANGS = new Set(["plaintext", "markdown", "log", "diff", "ignore", "gitattributes"]); +const NO_INDENT_LANGS = new Set(["plaintext", "markdown", "asciidoc", "log", "diff", "ignore", "gitattributes"]); function opensBlock(code: string, language: string | undefined): boolean { if (!code) return false;