From 7b3d7b2936fa6ad7e75ff02f58dc62702a0e8cfb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 11:47:28 -0500 Subject: [PATCH 01/15] Add runtime language detection --- .github/workflows/main.yml | 8 +- .github/workflows/nightly.yml | 6 +- Cargo.lock | 16 ++ Cargo.toml | 8 +- README.md | 17 +- build.rs | 19 -- code-editor/Cargo.toml | 6 +- code-editor/README.md | 2 +- code-editor/src/lib.rs | 40 ++-- code-editor/src/main.rs | 38 ++++ dioxus-code-macro/README.md | 2 +- docsite/Cargo.toml | 2 +- docsite/assets/app.css | 1 + docsite/snippets/runtime.rs | 2 +- docsite/src/components/card/style.css | 2 + docsite/src/main.rs | 28 +-- examples/basic/src/main.rs | 3 +- examples/editor/Cargo.toml | 6 +- examples/editor/src/main.rs | 10 +- examples/live-input/src/main.rs | 2 +- src/advanced.rs | 283 +++++++++----------------- src/language.rs | 43 +++- src/lib.rs | 86 +++++--- src/wasm_ctype.rs | 64 ++++++ 24 files changed, 393 insertions(+), 301 deletions(-) create mode 100644 code-editor/src/main.rs create mode 100644 src/wasm_ctype.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a49853c..311f170 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,13 +20,13 @@ jobs: with: all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web test: uses: dioxuslabs/dioxus-ci/.github/workflows/test.yml@v0.1.0 with: no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web fmt: uses: dioxuslabs/dioxus-ci/.github/workflows/fmt.yml@v0.1.0 @@ -36,11 +36,11 @@ jobs: with: all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web clippy: uses: dioxuslabs/dioxus-ci/.github/workflows/clippy.yml@v0.1.0 with: all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ef621d3..cfd98ed 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -19,14 +19,14 @@ jobs: toolchain: nightly all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web test: uses: dioxuslabs/dioxus-ci/.github/workflows/test.yml@v0.1.0 with: toolchain: nightly no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web clippy: uses: dioxuslabs/dioxus-ci/.github/workflows/clippy.yml@v0.1.0 @@ -34,7 +34,7 @@ jobs: toolchain: nightly all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web web-demo: uses: dioxuslabs/dioxus-ci/.github/workflows/web-build.yml@v0.1.0 diff --git a/Cargo.lock b/Cargo.lock index 7ad286f..4749f6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1644,6 +1644,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "betlang" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a5811a1a59386e19785588e5bb2384b7d543b4870acedd1c7a1cb177b13b7d" +dependencies = [ + "fearless_simd", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -2394,6 +2403,7 @@ dependencies = [ "arborium", "arborium-theme", "arborium-tree-sitter", + "betlang", "dioxus", "dioxus-code", "dioxus-code-editor", @@ -3322,6 +3332,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76258897e51fd156ee03b6246ea53f3e0eb395d0b327e9961c4fc4c8b2fa151a" + [[package]] name = "field-offset" version = "0.3.6" diff --git a/Cargo.toml b/Cargo.toml index 9c17529..aba2155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ categories = ["gui", "web-programming"] [workspace.dependencies] dioxus-code = { version = "0.1.1", path = "." } dioxus-code-editor = { version = "0.1.2", path = "code-editor", default-features = false } +betlang = { version = "0.1.0" } [package] name = "dioxus-code" @@ -43,7 +44,11 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["macro"] macro = ["dep:dioxus-code-macro", "dioxus-code-macro/lang-rust"] -runtime = ["arborium/lang-rust", "dep:arborium-tree-sitter"] +runtime = [ + "arborium/lang-rust", + "dep:arborium-tree-sitter", + "dep:betlang", +] all-languages = [ "runtime", "arborium/all-languages", @@ -260,6 +265,7 @@ arborium-theme = "2.16.0" arborium-tree-sitter = { version = "2.16.0", optional = true } dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } dioxus-code-macro = { version = "0.1.0", path = "dioxus-code-macro", default-features = false, optional = true } +betlang = { workspace = true, optional = true } [build-dependencies] arborium = { version = "2.16.0", default-features = false } diff --git a/README.md b/README.md index 5b37180..14de30a 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ A small Dioxus component for rendering source code with proper highlighting. Par Two ways to highlight: -- **[`code!`] macro** — parses at compile time and embeds the highlighted spans. Default. -- **[`SourceCode`]** — parses at runtime. Opt in with the `runtime` feature for dynamic source text. +- **[`code!`] macro** — parses at compile time. The runtime ships only the spans, no parser. Default. +- **[`SourceCode`]** — parses at runtime. Opt in with the `runtime` feature when the source isn't known until the user types it. ## Quick start @@ -51,7 +51,7 @@ When the file extension is ambiguous, pass [`CodeOptions::builder`] with [`CodeO ## Runtime highlighting -For editor-style use cases with dynamic source text: +For editor-style use cases where the source isn't known at compile time: ```toml [dependencies] @@ -60,23 +60,24 @@ dioxus-code = { version = "0.1", features = ["runtime"] } ```rust # use dioxus::prelude::*; -use dioxus_code::{Code, Language, SourceCode, Theme}; +use dioxus_code::{Code, CodeOptions, Language, SourceCode, Theme}; # let user_input = String::new(); # let _ = rsx! { Code { - src: SourceCode::new(Language::Rust, user_input), + src: SourceCode::new(user_input) + .with_options(CodeOptions::builder().with_language(Language::Rust)), theme: Theme::GITHUB_LIGHT, } } # ; ``` -Pass a [`Language`] variant when building [`SourceCode`]. The `runtime` feature includes Rust; enable the matching `lang-*` feature, or `all-languages`, for additional grammars. +Language can be set explicitly with the same [`CodeOptions`] builder used by [`code!`], or auto-detected from the source. The default `runtime` feature includes Rust; pass `lang-python`, `lang-toml`, or `all-languages` for the rest. ## Editor -[`dioxus-code-editor`] is a sibling crate that pairs the highlighter with a textarea input layer: +[`dioxus-code-editor`] is a sibling crate that pairs the highlighter with a `contenteditable` input layer: ```rust # use dioxus::prelude::*; @@ -130,7 +131,7 @@ Code { ```sh dx serve --example dioxus-code-basic # macro + runtime side by side -dx serve --example dioxus-code-macro-only # compile-time highlighted spans +dx serve --example dioxus-code-macro-only # compile-time only, no parser in the binary dx serve --example dioxus-code-live-input # textarea bound to runtime highlighter ``` diff --git a/build.rs b/build.rs index ea0523e..700cfdd 100644 --- a/build.rs +++ b/build.rs @@ -104,25 +104,6 @@ fn main() { )); } - generated.push_str( - r#" /// Every syntax theme, in declaration order. - /// - /// ```rust - /// use dioxus_code::Theme; - /// assert!(Theme::ALL.contains(&Theme::TOKYO_NIGHT)); - /// ``` - pub const ALL: &'static [Theme] = &[ -"#, - ); - for theme in &themes { - generated.push_str(&format!(" Self::{},\n", theme.const_name)); - } - generated.push_str( - r#" ]; - -"#, - ); - generated.push_str( r#"} "#, diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 2a3505d..27a4fcc 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -17,7 +17,7 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } -dioxus-code = { workspace = true, features = ["runtime"] } +dioxus-code = { workspace = true, features = ["lang-toml", "lang-python"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2" @@ -30,4 +30,6 @@ web-sys = { version = "0.3", features = [ ] } [features] -all-languages = ["dioxus-code/all-languages"] +default = ["desktop"] +desktop = ["dioxus/desktop", "dioxus/launch"] +web = ["dioxus/web", "dioxus/launch"] diff --git a/code-editor/README.md b/code-editor/README.md index d8e59d1..d71a2be 100644 --- a/code-editor/README.md +++ b/code-editor/README.md @@ -53,7 +53,7 @@ The component is controlled — drive [`CodeEditorProps::value`] from your own s | prop | description | |---|---| | [`CodeEditorProps::value`] | Current editor contents. | -| [`CodeEditorProps::language`] | Syntax grammar selection. Pass a [`Language`] variant (for example [`Language::Rust`]) or use [`Language::from_slug`] for runtime slugs. | +| [`CodeEditorProps::language`] | Tree-sitter grammar selection. Pass a [`Language`] variant (for example [`Language::Rust`]) or use [`Language::from_slug`] for custom slugs. | | [`CodeEditorProps::theme`] | Syntax theme selection shared with [`dioxus-code`](https://crates.io/crates/dioxus-code); accepts [`Theme`] or [`CodeTheme`]. | | [`CodeEditorProps::line_numbers`] | Show a one-based line gutter. Defaults to `true`. | | [`CodeEditorProps::read_only`] | Disable editing while preserving highlighting. | diff --git a/code-editor/src/lib.rs b/code-editor/src/lib.rs index 7e0e7a2..497bbc3 100644 --- a/code-editor/src/lib.rs +++ b/code-editor/src/lib.rs @@ -5,7 +5,7 @@ use dioxus::prelude::*; pub use dioxus_code::Language; #[cfg(test)] use dioxus_code::Theme; -use dioxus_code::advanced::{Buffer, CodeThemeStyles, HighlightError, TokenSpan}; +use dioxus_code::advanced::{Buffer, CodeThemeStyles, TokenSpan}; #[cfg(test)] use dioxus_code::advanced::{HighlightSegment, HighlightedSource}; use dioxus_code::{CodeTheme, SourceCode}; @@ -71,11 +71,6 @@ pub struct CodeEditorProps { pub oninput: EventHandler, } -struct EditorBuffer { - buffer: Option, - language: Language, -} - /// Editable syntax-highlighted code surface. /// /// The component is controlled by [`CodeEditorProps::value`]; update that value @@ -101,15 +96,10 @@ struct EditorBuffer { /// ``` #[component] pub fn CodeEditor(props: CodeEditorProps) -> Element { - let state = use_hook({ + let buffer = use_hook({ let value = props.value.clone(); let language = props.language; - move || { - Rc::new(RefCell::new(EditorBuffer { - buffer: Buffer::new(language, value).ok(), - language, - })) - } + move || Rc::new(RefCell::new(Buffer::new(language, value).ok())) }); let edit_tracker = use_hook(|| { Rc::new(RefCell::new(edit_capture::InputEditTracker::new( @@ -119,30 +109,28 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { let edit = edit_tracker.borrow_mut().take_for_render(&props.value); let snapshot = { - let mut slot = state.borrow_mut(); - if slot.language != props.language { - slot.buffer = Buffer::new(props.language, props.value.clone()).ok(); - slot.language = props.language; + let mut buffer_slot = buffer.borrow_mut(); + if buffer_slot.is_none() { + *buffer_slot = Buffer::new(props.language, props.value.clone()).ok(); } - match slot.buffer.as_mut() { + match buffer_slot.as_mut() { Some(buffer) => { + if buffer.language() != props.language { + let _ = buffer.set_language(props.language); + } if buffer.source() != props.value { let result = match edit { - Some(edit) => match buffer.edit(edit, props.value.clone()) { - Ok(()) => Ok(()), - Err(HighlightError::InvalidEdit { .. }) => { - buffer.replace(props.value.clone()) - } - Err(error) => Err(error), - }, + Some(edit) => buffer.edit(edit, props.value.clone()), None => buffer.replace(props.value.clone()), }; let _ = result; } buffer.highlighted() } - None => SourceCode::new(props.language, props.value.clone()).into(), + None => SourceCode::new(props.value.clone()) + .with_language(props.language) + .into(), } }; let lines = snapshot.lines(); diff --git a/code-editor/src/main.rs b/code-editor/src/main.rs new file mode 100644 index 0000000..7125e48 --- /dev/null +++ b/code-editor/src/main.rs @@ -0,0 +1,38 @@ +use dioxus::prelude::*; +use dioxus_code::Theme; +use dioxus_code_editor::{CodeEditor, Language}; + +const DEMO_CSS: Asset = asset!("/assets/demo.css"); + +const STARTER: &str = r#"pub fn luminance(rgb: (u8, u8, u8)) -> f32 { + let (r, g, b) = rgb; + 0.2126 * r as f32 + 0.7152 * g as f32 + 0.0722 * b as f32 +} +"#; + +fn main() { + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + let mut source = use_signal(|| STARTER.to_string()); + let language = Language::detect(&source()).unwrap_or(Language::Rust); + let language_label = language.slug(); + + rsx! { + document::Stylesheet { href: DEMO_CSS } + main { class: "shell", + section { class: "toolbar", + h1 { "Code editor component" } + span { "{language_label}" } + } + CodeEditor { + value: source(), + language, + theme: Theme::TOKYO_NIGHT, + oninput: move |value| source.set(value), + } + } + } +} diff --git a/dioxus-code-macro/README.md b/dioxus-code-macro/README.md index c3e914b..d9d55ad 100644 --- a/dioxus-code-macro/README.md +++ b/dioxus-code-macro/README.md @@ -19,7 +19,7 @@ Implementation crate for the [`code!`] macro re-exported by [`dioxus-code`](https://crates.io/crates/dioxus-code) under its default `macro` feature. You usually depend on `dioxus-code` instead of pulling this in directly. -The macro reads a source file at compile time, parses it with [`arborium`](https://crates.io/crates/arborium), and expands to a static span tree that can be rendered by `dioxus-code`. +The macro reads a source file at compile time, parses it with [`arborium`](https://crates.io/crates/arborium), and expands to a static span tree. The runtime binary ships only the spans — no parser. ```rust use dioxus_code::code; diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index c63503c..4b44fdb 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] dioxus = { version = "0.7.0", features = ["router"] } -dioxus-code = { workspace = true, features = ["runtime", "lang-toml"] } +dioxus-code = { workspace = true, features = ["all-languages"] } dioxus-code-editor = { workspace = true } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } diff --git a/docsite/assets/app.css b/docsite/assets/app.css index 47512b6..e80b816 100644 --- a/docsite/assets/app.css +++ b/docsite/assets/app.css @@ -325,6 +325,7 @@ a { font-size: 13px; line-height: 1.65; margin: 0; + min-height: 380px; padding: 18px 20px; } diff --git a/docsite/snippets/runtime.rs b/docsite/snippets/runtime.rs index 774300f..4ef1257 100644 --- a/docsite/snippets/runtime.rs +++ b/docsite/snippets/runtime.rs @@ -11,7 +11,7 @@ fn App() -> Element { rsx! { Code { - src: SourceCode::new(Language::Rust, source()), + src: SourceCode::new(source()).with_language(Language::Rust), theme: CodeTheme::system(Theme::GITHUB_LIGHT, Theme::GITHUB_DARK), } } diff --git a/docsite/src/components/card/style.css b/docsite/src/components/card/style.css index 2bad15f..de5cfa7 100644 --- a/docsite/src/components/card/style.css +++ b/docsite/src/components/card/style.css @@ -1,11 +1,13 @@ .card { display: flex; flex-direction: column; + padding: 1.5rem 0; border: 1px solid var(--light, var(--primary-color-6)) var(--dark, var(--primary-color-5)); border-radius: 1rem; background-color: var(--light, var(--primary-color-2)) var(--dark, var(--primary-color-3)); box-shadow: 0 2px 10px rgb(0 0 0 / 10%); color: var(--secondary-color-4); + gap: 1.5rem; } .card-header { diff --git a/docsite/src/main.rs b/docsite/src/main.rs index 6a08fd0..89861b5 100644 --- a/docsite/src/main.rs +++ b/docsite/src/main.rs @@ -544,7 +544,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { "." } p { class: "hero-lede", - "A drop-in component with two source modes: compile-time macro and runtime highlighting with explicit language selection." + "A drop-in component with two source modes — compile-time macro and runtime detection. No JS, no flash of unstyled code." } div { class: "hero-terminal-block", div { class: "hero-terminal-bar", @@ -577,7 +577,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { span { "{theme_label}" } } div { class: "card-code-body", - Code { src: SourceCode::new(Language::Rust, HERO_COUNTER), theme } + Code { src: SourceCode::new(HERO_COUNTER).with_language(Language::Rust), theme } } } } @@ -640,15 +640,15 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-value", "OPT-IN" } } li { class: "receipt-item receipt-optional", - span { class: "receipt-label", "Runtime grammars" } + span { class: "receipt-label", "Tree-sitter grammars" } span { class: "receipt-dots" } span { class: "receipt-value", "+3.33 MiB" } } } div { class: "receipt-rule double" } div { class: "receipt-total", - span { class: "receipt-total-label", "COMPILE-TIME MODE" } - span { class: "receipt-total-value", "STATIC" } + span { class: "receipt-total-label", "PARSER BYTES SHIPPED" } + span { class: "receipt-total-value", "0" } } } aside { class: "receipt-aside", @@ -656,14 +656,14 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-aside-num", "01" } div { h3 { class: "receipt-aside-title", "code!" } - p { class: "receipt-aside-text", "Tokenizes during cargo build and embeds highlighted spans for rendering." } + p { class: "receipt-aside-text", "Tokenizes during cargo build. The runtime gets pre-styled markup with no parser bytes." } } } div { class: "receipt-aside-row", span { class: "receipt-aside-num", "02" } div { h3 { class: "receipt-aside-title", "SourceCode" } - p { class: "receipt-aside-text", "Pull it in when input is dynamic and pass the language your source uses." } + p { class: "receipt-aside-text", "Pull it in when input is dynamic. Tree-sitter grammars detect language automatically." } } } div { class: "receipt-aside-row", @@ -690,6 +690,9 @@ fn Playground( let theme_pair = theme_pairs[active_idx()]; let theme = theme_pair.code_theme(scheme); let value = use_memo(move || Some(active_idx())); + let language = Language::detect(&source()).unwrap_or(Language::Rust); + let language_label = language.slug(); + let source_len = source().chars().count(); rsx! { section { id: "playground", class: "section", @@ -701,9 +704,9 @@ fn Playground( div { class: "playground-grid", Card { class: "card-editor", div { class: "card-bar", - span { "source.rs" } + span { "source" } span { class: "editor-meta", - span { "rust · " {format!("{} chars", source().chars().count())} } + span { "{language_label} · {source_len} chars" } span { class: "editor-meta-divider" } Select:: { value: Some(value.into()), @@ -732,9 +735,10 @@ fn Playground( ClientOnly { CodeEditor { value: source(), - language: Language::Rust, + language, theme, - aria_label: "Rust source editor", + aria_label: "Source editor", + placeholder: "Type code...", class: "playground-code-editor", oninput: move |value| source.set(value), } @@ -781,7 +785,7 @@ fn Docs(scheme: Scheme) -> Element { } div { class: "card-code-body", Code { - src: SourceCode::new(step.language, step.code), + src: SourceCode::new(step.code).with_language(step.language), theme, } } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 12d60c0..f002a70 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -15,7 +15,8 @@ fn App() -> Element { theme: Theme::RUSTDOC_AYU, } Code { - src: SourceCode::new(Language::Rust, "fn main() {\n println!(\"runtime\");\n}"), + src: SourceCode::new("fn main() {\n println!(\"runtime\");\n}") + .with_language(Language::Rust), theme: Theme::GITHUB_LIGHT, } } diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index b80fd6a..5533d59 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -11,10 +11,10 @@ publish = false [dependencies] dioxus = { version = "0.7.0" } -dioxus-code = { workspace = true, features = ["runtime"] } +dioxus-code = { workspace = true, features = ["all-languages"] } dioxus-code-editor = { workspace = true } [features] default = ["desktop"] -desktop = ["dioxus/desktop"] -web = ["dioxus/web"] +desktop = ["dioxus/desktop", "dioxus-code-editor/desktop"] +web = ["dioxus/web", "dioxus-code-editor/web"] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 91765e6..44957e2 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -33,6 +33,8 @@ fn App() -> Element { let mut read_only = use_signal(|| false); let (theme_label, theme) = THEMES[theme_index()]; + let language = Language::detect(&source()).unwrap_or(Language::Rust); + let language_label = language.slug(); rsx! { style { {APP_CSS} } @@ -82,17 +84,17 @@ fn App() -> Element { } section { class: "editor-frame", div { class: "frame-bar", - span { "fizzbuzz.rs" } + span { "{language_label}" } span { "{theme_label}" } } CodeEditor { value: source(), - language: Language::Rust, + language, theme: CodeTheme::fixed(theme), line_numbers: line_numbers(), read_only: read_only(), - aria_label: "Rust source editor", - placeholder: "Type Rust code...", + aria_label: "Source editor", + placeholder: "Type code...", class: "example-editor", oninput: move |value| source.set(value), } diff --git a/examples/live-input/src/main.rs b/examples/live-input/src/main.rs index 66d0fa1..adc3fbd 100644 --- a/examples/live-input/src/main.rs +++ b/examples/live-input/src/main.rs @@ -36,7 +36,7 @@ fn App() -> Element { span { "runtime" } } Code { - src: SourceCode::new(Language::Rust, source()), + src: SourceCode::new(source()).with_language(Language::Rust), theme: Theme::TOKYO_NIGHT, } } diff --git a/src/advanced.rs b/src/advanced.rs index afb7157..b9ab4d2 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -22,6 +22,8 @@ use std::{borrow::Cow, fmt, ops::Range}; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum HighlightError { + /// No language could be detected and no explicit language was supplied. + LanguageDetectionFailed, /// The tree-sitter parser rejected the selected grammar. GrammarLoad { /// The language whose grammar failed to load. @@ -49,22 +51,6 @@ pub enum HighlightError { /// The language being parsed. language: Language, }, - /// [`Buffer::edit`] received offsets that are out of bounds or not on - /// UTF-8 character boundaries. - /// - /// The buffer's state is unchanged when this error is returned. - InvalidEdit { - /// First byte the caller said changed. - start_byte: usize, - /// One past the last byte of the replaced region in the previous source. - old_end_byte: usize, - /// One past the last byte of the inserted region in the new source. - new_end_byte: usize, - /// Length of the previous source, for context. - old_len: usize, - /// Length of the new source, for context. - new_len: usize, - }, } impl HighlightError { @@ -92,6 +78,7 @@ impl HighlightError { impl fmt::Display for HighlightError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::LanguageDetectionFailed => write!(f, "could not detect language"), Self::GrammarLoad { language, message } => { write!( f, @@ -121,19 +108,6 @@ impl fmt::Display for HighlightError { Self::Parse { language } => { write!(f, "tree-sitter parse failed for {}", language.slug()) } - Self::InvalidEdit { - start_byte, - old_end_byte, - new_end_byte, - old_len, - new_len, - } => { - write!( - f, - "invalid edit: start_byte {start_byte}, old_end_byte {old_end_byte}, \ - new_end_byte {new_end_byte} (old_len {old_len}, new_len {new_len})" - ) - } } } } @@ -201,7 +175,7 @@ impl From for HighlightQueryErrorKind { #[derive(Debug, Clone, PartialEq, Eq)] pub struct HighlightedSource { source: Cow<'static, str>, - language: Language, + language: Option, spans: Cow<'static, [HighlightSpan]>, } @@ -209,7 +183,7 @@ impl HighlightedSource { #[cfg(feature = "runtime")] pub(crate) fn from_owned_parts( source: String, - language: Language, + language: Option, spans: Vec, ) -> Self { Self { @@ -227,7 +201,7 @@ impl HighlightedSource { /// use dioxus_code::Language; /// use dioxus_code::advanced::HighlightedSource; /// let src = HighlightedSource::from_static_parts("let x = 1;", Language::Rust, &[]); - /// assert_eq!(src.language(), Language::Rust); + /// assert_eq!(src.language(), Some(Language::Rust)); /// ``` pub const fn from_static_parts( source: &'static str, @@ -236,13 +210,16 @@ impl HighlightedSource { ) -> Self { Self { source: Cow::Borrowed(source), - language, + language: Some(language), spans: Cow::Borrowed(spans), } } #[cfg(feature = "runtime")] - pub(crate) fn plaintext(source: impl Into>, language: Language) -> Self { + pub(crate) fn plaintext( + source: impl Into>, + language: Option, + ) -> Self { Self { source: source.into(), language, @@ -268,9 +245,9 @@ impl HighlightedSource { /// use dioxus_code::Language; /// use dioxus_code::advanced::HighlightedSource; /// let src = HighlightedSource::from_static_parts("", Language::Rust, &[]); - /// assert_eq!(src.language(), Language::Rust); + /// assert_eq!(src.language(), Some(Language::Rust)); /// ``` - pub const fn language(&self) -> Language { + pub const fn language(&self) -> Option { self.language } @@ -580,7 +557,7 @@ pub fn TokenSpan(props: TokenSpanProps) -> Element { /// a single coherent unit. [`edit`](Self::edit) applies an incremental edit /// (reusing the cached parse tree); [`replace`](Self::replace) swaps the source /// wholesale; [`set_language`](Self::set_language) switches grammars and -/// reparses. After any successful mutation, [`source`](Self::source), +/// reparses. After any mutation, [`source`](Self::source), /// [`spans`](Self::spans), and [`lines`](Self::lines) reflect the new state. /// /// Available with the `runtime` feature. Hold one per editor instance (e.g. @@ -600,8 +577,8 @@ pub struct Buffer { parser: arborium_tree_sitter::Parser, cursor: arborium_tree_sitter::QueryCursor, language: Language, - incremental: IncrementalGrammar, - tree: arborium_tree_sitter::Tree, + incremental: Option, + tree: Option, source: String, spans: Vec, } @@ -622,28 +599,19 @@ impl Buffer { /// let buffer = Buffer::new(Language::Rust, "fn main() {}").expect("rust grammar loads"); /// assert_eq!(buffer.source(), "fn main() {}"); /// ``` - pub fn new(language: Language, source: impl ToString) -> Result { - let source = source.to_string(); - let (mut parser, incremental) = Self::parser_for(language)?; - let mut cursor = arborium_tree_sitter::QueryCursor::new(); - let (tree, spans) = Self::parse_source( + pub fn new(language: Language, source: impl Into) -> Result { + let mut buffer = Self { + parser: arborium_tree_sitter::Parser::new(), + cursor: arborium_tree_sitter::QueryCursor::new(), language, - &mut parser, - &incremental.query, - &mut cursor, - &source, - None, - )?; - - Ok(Self { - parser, - cursor, - language, - incremental, - tree, - source, - spans, - }) + incremental: None, + tree: None, + source: String::new(), + spans: Vec::new(), + }; + buffer.install_grammar(language)?; + buffer.replace(source)?; + Ok(buffer) } /// Replace the source wholesale and reparse from scratch. @@ -659,21 +627,10 @@ impl Buffer { /// buffer.replace("fn new() {}").expect("rust parses"); /// assert_eq!(buffer.source(), "fn new() {}"); /// ``` - pub fn replace(&mut self, source: impl ToString) -> Result<(), HighlightError> { - let source = source.to_string(); - let (tree, spans) = Self::parse_source( - self.language, - &mut self.parser, - &self.incremental.query, - &mut self.cursor, - &source, - None, - )?; - - self.source = source; - self.tree = tree; - self.spans = spans; - Ok(()) + pub fn replace(&mut self, source: impl Into) -> Result<(), HighlightError> { + self.source = source.into(); + self.tree = None; + self.reparse() } /// Apply an incremental edit and reparse, reusing the cached parse tree. @@ -681,10 +638,8 @@ impl Buffer { /// `new_source` must be the full text *after* the edit. `edit` describes /// the byte range that changed — its `start_byte` / `old_end_byte` index /// into the buffer's previous source, and `new_end_byte` indexes into - /// `new_source`. If the edit is malformed, [`HighlightError::InvalidEdit`] - /// is returned and the buffer is left unchanged. Validation is limited to - /// bounds and UTF-8 character boundaries; callers are responsible for - /// passing an edit range that matches the unchanged prefix and suffix. + /// `new_source`. If the edit is malformed, the cached tree is dropped and + /// the new source is parsed from scratch. /// /// ```rust /// use dioxus_code::Language; @@ -699,26 +654,19 @@ impl Buffer { pub fn edit( &mut self, edit: SourceEdit, - new_source: impl ToString, + new_source: impl Into, ) -> Result<(), HighlightError> { - let new_source: String = new_source.to_string(); - let input_edit = edit.into_input_edit(&self.source, &new_source)?; - - let mut old_tree = self.tree.clone(); - old_tree.edit(&input_edit); - let (tree, spans) = Self::parse_source( - self.language, - &mut self.parser, - &self.incremental.query, - &mut self.cursor, - &new_source, - Some(&old_tree), - )?; - + let new_source: String = new_source.into(); + if let (Some(_), Some(tree)) = (&self.incremental, self.tree.as_mut()) { + match edit.into_input_edit(&self.source, &new_source) { + Some(input_edit) => tree.edit(&input_edit), + None => self.tree = None, + } + } else { + self.tree = None; + } self.source = new_source; - self.tree = tree; - self.spans = spans; - Ok(()) + self.reparse() } /// Switch grammars and reparse the current source. @@ -736,22 +684,9 @@ impl Buffer { if self.language == language { return Ok(()); } - let (mut parser, incremental) = Self::parser_for(language)?; - let (tree, spans) = Self::parse_source( - language, - &mut parser, - &incremental.query, - &mut self.cursor, - &self.source, - None, - )?; - - self.parser = parser; - self.incremental = incremental; - self.language = language; - self.tree = tree; - self.spans = spans; - Ok(()) + self.install_grammar(language)?; + self.tree = None; + self.reparse() } /// The current source text. @@ -784,39 +719,56 @@ impl Buffer { /// Useful for handing off to [`Code()`](crate::Code()) or any consumer /// that takes the frozen snapshot type. pub fn highlighted(&self) -> HighlightedSource { - HighlightedSource::from_owned_parts(self.source.clone(), self.language, self.spans.clone()) + HighlightedSource::from_owned_parts( + self.source.clone(), + Some(self.language), + self.spans.clone(), + ) } - fn parser_for( - language: Language, - ) -> Result<(arborium_tree_sitter::Parser, IncrementalGrammar), HighlightError> { - let mut parser = arborium_tree_sitter::Parser::new(); + fn install_grammar(&mut self, language: Language) -> Result<(), HighlightError> { + self.language = language; let (language_fn, highlights_query) = grammar_for(language); let ts_language: arborium_tree_sitter::Language = language_fn.into(); - if let Err(error) = parser.set_language(&ts_language) { - return Err(HighlightError::grammar_load(language, error)); + if let Err(error) = self.parser.set_language(&ts_language) { + return self.fail(HighlightError::grammar_load(language, error)); } match arborium_tree_sitter::Query::new(&ts_language, highlights_query) { - Ok(query) => Ok((parser, IncrementalGrammar { query })), - Err(error) => Err(HighlightError::query(language, error)), + Ok(query) => { + self.incremental = Some(IncrementalGrammar { query }); + Ok(()) + } + Err(error) => self.fail(HighlightError::query(language, error)), } } - fn parse_source( - language: Language, - parser: &mut arborium_tree_sitter::Parser, - query: &arborium_tree_sitter::Query, - cursor: &mut arborium_tree_sitter::QueryCursor, - source: &str, - old_tree: Option<&arborium_tree_sitter::Tree>, - ) -> Result<(arborium_tree_sitter::Tree, Vec), HighlightError> { - match parser.parse(source, old_tree) { + fn fail(&mut self, error: HighlightError) -> Result { + self.incremental = None; + self.tree = None; + self.spans.clear(); + Err(error) + } + + fn reparse(&mut self) -> Result<(), HighlightError> { + let Some(grammar) = &self.incremental else { + self.tree = None; + self.spans.clear(); + return Err(HighlightError::GrammarLoad { + language: self.language, + message: "grammar is not loaded".to_owned(), + }); + }; + + match self.parser.parse(&self.source, self.tree.as_ref()) { Some(tree) => { - let spans = collect_spans(query, cursor, &tree, source); - Ok((tree, spans)) + self.spans = collect_spans(&grammar.query, &mut self.cursor, &tree, &self.source); + self.tree = Some(tree); + Ok(()) } - None => Err(HighlightError::Parse { language }), + None => self.fail(HighlightError::Parse { + language: self.language, + }), } } } @@ -1377,8 +1329,8 @@ fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'stati /// A byte-range edit description used to drive incremental highlighting. /// -/// Build one from a real edit signal (for example a textarea `beforeinput` -/// event) and pass it to [`Buffer::edit`]. `start_byte` and +/// Build one from a real edit signal (for example a contenteditable +/// `beforeinput` event) and pass it to [`Buffer::edit`]. `start_byte` and /// `old_end_byte` index into the buffer's previous source, while /// `new_end_byte` indexes into the new source supplied alongside the edit. /// @@ -1405,26 +1357,18 @@ impl SourceEdit { self, old_source: &str, new_source: &str, - ) -> Result { + ) -> Option { if self.start_byte > self.old_end_byte || self.start_byte > self.new_end_byte || self.old_end_byte > old_source.len() || self.new_end_byte > new_source.len() || !old_source.is_char_boundary(self.start_byte) || !old_source.is_char_boundary(self.old_end_byte) - || !new_source.is_char_boundary(self.start_byte) || !new_source.is_char_boundary(self.new_end_byte) { - return Err(HighlightError::InvalidEdit { - start_byte: self.start_byte, - old_end_byte: self.old_end_byte, - new_end_byte: self.new_end_byte, - old_len: old_source.len(), - new_len: new_source.len(), - }); + return None; } - - Ok(arborium_tree_sitter::InputEdit { + Some(arborium_tree_sitter::InputEdit { start_byte: self.start_byte, old_end_byte: self.old_end_byte, new_end_byte: self.new_end_byte, @@ -1499,7 +1443,9 @@ mod buffer_tests { } fn batch_spans(source: &str, language: Language) -> Vec { - let snapshot: HighlightedSource = SourceCode::new(language, source.to_owned()).into(); + let snapshot: HighlightedSource = SourceCode::new(source.to_owned()) + .with_language(language) + .into(); snapshot.spans().to_vec() } @@ -1538,52 +1484,25 @@ mod buffer_tests { } #[test] - fn malformed_edit_returns_typed_error_and_leaves_state_unchanged() { + fn malformed_edit_falls_back_to_full_parse() { let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap(); - let previous_spans = buffer.spans().to_vec(); let updated = "fn main() { let x = 12; }"; - - assert_eq!( - buffer.edit( - // old_end_byte beyond the previous source — must not panic. - SourceEdit { - start_byte: 21, - old_end_byte: 999, - new_end_byte: 22, - }, - updated, - ), - Err(HighlightError::InvalidEdit { - start_byte: 21, - old_end_byte: 999, - new_end_byte: 22, - old_len: "fn main() { let x = 1; }".len(), - new_len: updated.len(), - }), - ); - assert_eq!(buffer.source(), "fn main() { let x = 1; }"); - assert_eq!(buffer.spans(), previous_spans.as_slice()); - } - - #[test] - fn semantic_edit_mismatch_is_not_validated() { - let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap(); - let updated = "fn main() { let y = 1; }"; - buffer .edit( - // The unchanged suffix does not match this edit; validation - // intentionally stays O(1) and trusts callers on semantics. + // old_end_byte beyond the previous source — must not panic. SourceEdit { start_byte: 21, - old_end_byte: 21, + old_end_byte: 999, new_end_byte: 22, }, updated, ) .unwrap(); - assert_eq!(buffer.source(), updated); + assert_eq!( + span_ranges(buffer.spans()), + span_ranges(&batch_spans(updated, Language::Rust)), + ); } #[test] diff --git a/src/language.rs b/src/language.rs index 8f9c5fe..25122b3 100644 --- a/src/language.rs +++ b/src/language.rs @@ -2,7 +2,10 @@ //! //! [`Language`] is a closed set of Arborium language slugs. Each named variant //! is gated by the same cargo feature as the corresponding grammar in -//! `dioxus-code`, so the enum exposes the variants compiled into this build. +//! `dioxus-code`, so the enum only ever exposes variants whose grammar is +//! actually compiled into this build. + +use dioxus::core::SuperFrom; macro_rules! define_languages { ( @@ -15,8 +18,8 @@ macro_rules! define_languages { /// /// Each variant maps to an Arborium language slug via /// [`Language::slug`]. Variants are gated by the same `lang-*` cargo - /// features as the grammar lookup table, so each build exposes the - /// variants enabled for that build. + /// features as the grammar lookup table, so unsupported builds simply + /// don't expose those variants. /// /// ```rust /// use dioxus_code::Language; @@ -80,15 +83,23 @@ macro_rules! define_languages { impl Language { /// Best-effort detection from a path, filename, shebang, or file contents. /// - /// Wraps [`arborium::detect_language`] and maps the resulting slug into a - /// [`Language`] variant, returning `None` when detection fails or the - /// detected language's grammar feature is disabled in this build. + /// First uses Arborium's path and shebang detector, then falls back to + /// betlang source-language inference from source text. The + /// detected slug is mapped into a [`Language`] variant, returning `None` + /// when detection fails or the detected language's grammar feature is + /// disabled in this build. /// /// Available with the `runtime` feature. #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] pub fn detect(input: &str) -> Option { - arborium::detect_language(input).and_then(Self::from_slug) + arborium::detect_language(input) + .and_then(Self::from_slug) + .or_else(|| { + betlang::detect(input) + .language() + .and_then(|language| Self::from_slug(language.slug())) + }) } } @@ -299,3 +310,21 @@ define_languages! { #[cfg(feature = "lang-zsh")] Zsh => "zsh", } + +#[doc(hidden)] +pub struct OptionLanguageFromStrMarker; + +impl<'a> SuperFrom<&'a str, OptionLanguageFromStrMarker> for Option { + fn super_from(slug: &'a str) -> Self { + Language::from_slug(slug) + } +} + +#[doc(hidden)] +pub struct OptionLanguageFromStringMarker; + +impl SuperFrom for Option { + fn super_from(slug: String) -> Self { + Language::from_slug(&slug) + } +} diff --git a/src/lib.rs b/src/lib.rs index 158c6c4..1cd3c85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,19 +11,23 @@ use std::collections::HashMap; mod language; pub use language::Language; +#[cfg(all(feature = "runtime", target_family = "wasm"))] +mod wasm_ctype; + const CODE_CSS: Asset = asset!("/assets/dioxus-code.css"); #[cfg(feature = "macro")] #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] pub use dioxus_code_macro::{code, code_str}; -/// Compile-time options for the [`code!`] and [`code_str!`] macros. +/// Options shared by the [`code!`] and [`code_str!`] macros and runtime +/// [`SourceCode`]. /// -/// Both macros read this builder syntactically; pass +/// The macros read this builder syntactically at compile time; pass /// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to override the /// language that would otherwise be inferred from the file extension. For -/// [`code_str!`] the language is required since there is no extension to -/// infer from. +/// [`code_str!`] the language is required since there is no extension to infer +/// from. [`SourceCode`] consumes the same builder at runtime. /// /// ```rust /// use dioxus_code::{CodeOptions, Language, code}; @@ -215,47 +219,60 @@ pub use advanced::{HighlightError, HighlightQueryErrorKind}; /// Source text to highlight at runtime. /// /// Available with the `runtime` feature. Build one with [`SourceCode::new`], -/// then pass it to [`Code()`]. +/// optionally annotate it with [`SourceCode::with_language`], then pass it to +/// [`Code()`]. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; -/// let _src = SourceCode::new(Language::Rust, "fn main() {}"); +/// let _src = SourceCode::new("fn main() {}").with_language(Language::Rust); /// ``` #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceCode { source: String, - language: Language, + options: CodeOptions, } #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] impl SourceCode { - /// Wrap a raw source string with an explicit language. + /// Wrap a raw source string with no language hint. /// /// ```rust - /// use dioxus_code::{Language, SourceCode}; - /// let _src = SourceCode::new(Language::Rust, "fn main() {}"); + /// use dioxus_code::SourceCode; + /// let _src = SourceCode::new("fn main() {}"); /// ``` - pub fn new(language: Language, source: impl ToString) -> Self { + pub fn new(source: impl Into) -> Self { Self { - source: source.to_string(), - language, + source: source.into(), + options: CodeOptions::new(), } } - /// Replace the language used to highlight this source. + /// Apply shared [`CodeOptions`]. + /// + /// ```rust + /// use dioxus_code::{CodeOptions, Language, SourceCode}; + /// let options = CodeOptions::builder().with_language(Language::Rust); + /// let _src = SourceCode::new("fn main() {}").with_options(options); + /// ``` + pub fn with_options(mut self, options: CodeOptions) -> Self { + self.options = options; + self + } + + /// Set the language explicitly. /// /// To set the language from a runtime slug, use [`Language::from_slug`] /// and pass the resulting variant. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; - /// let _src = SourceCode::new(Language::Rust, "fn main() {}").with_language(Language::Rust); + /// let _src = SourceCode::new("fn main() {}").with_language(Language::Rust); /// ``` - pub fn with_language(mut self, language: Language) -> Self { - self.language = language; + pub fn with_language(mut self, language: impl Into>) -> Self { + self.options = self.options.with_language(language); self } @@ -264,12 +281,21 @@ impl SourceCode { /// Use `Into` for the lossy rendering path that discards /// the error and renders plaintext. pub fn highlight(self) -> Result { - advanced::Buffer::new(self.language, self.source).map(|buffer| buffer.highlighted()) + let language = self + .options + .language() + .or_else(|| Language::detect(&self.source)); + match language { + Some(language) => { + advanced::Buffer::new(language, self.source).map(|buffer| buffer.highlighted()) + } + None => Err(HighlightError::LanguageDetectionFailed), + } } fn highlight_or_plaintext(self) -> advanced::HighlightedSource { - let language = self.language; let source = self.source.clone(); + let language = self.options.language(); match self.highlight() { Ok(source) => source, Err(_) => advanced::HighlightedSource::plaintext(source, language), @@ -382,7 +408,7 @@ pub fn Code(props: CodeProps) -> Element { let source = &props.src; let segments = source.trimmed_segments(); let class = format!("dxc {}", props.theme.classes()); - let language = source.language().slug(); + let language = source.language().map(Language::slug).unwrap_or("text"); rsx! { advanced::CodeThemeStyles { theme: props.theme } @@ -468,10 +494,22 @@ mod tests { #[cfg(feature = "runtime")] #[test] - fn runtime_source_code_highlights() { + fn runtime_code_options_highlights() { + let tree: advanced::HighlightedSource = SourceCode::new("fn main() {}") + .with_options(CodeOptions::builder().with_language(Language::Rust)) + .into(); + assert_eq!(tree.language(), Some(Language::Rust)); + assert!(tree.spans().iter().any(|span| { + span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" + })); + } + + #[cfg(feature = "runtime")] + #[test] + fn runtime_raw_string_detects_language_from_source() { let tree: advanced::HighlightedSource = - SourceCode::new(Language::Rust, "fn main() {}").into(); - assert_eq!(tree.language(), Language::Rust); + SourceCode::new("use std::fmt;\nfn main() { println!(\"hi\"); }").into(); + assert_eq!(tree.language(), Some(Language::Rust)); assert!(tree.spans().iter().any(|span| { span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" })); @@ -484,7 +522,7 @@ mod tests { "fn main() {}", CodeOptions::builder().with_language(Language::Rust) ); - assert_eq!(TREE.language(), Language::Rust); + assert_eq!(TREE.language(), Some(Language::Rust)); assert_eq!(TREE.source(), "fn main() {}"); assert!(TREE.spans().iter().any(|span| { span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn" diff --git a/src/wasm_ctype.rs b/src/wasm_ctype.rs new file mode 100644 index 0000000..14dd20f --- /dev/null +++ b/src/wasm_ctype.rs @@ -0,0 +1,64 @@ +//! C ctype symbols needed by Arborium grammar scanners in WASM builds. + +#![allow(missing_docs)] + +use std::ffi::c_int; + +#[inline] +const fn is_lower(c: c_int) -> bool { + c >= b'a' as c_int && c <= b'z' as c_int +} + +#[inline] +const fn is_upper(c: c_int) -> bool { + c >= b'A' as c_int && c <= b'Z' as c_int +} + +#[inline] +const fn is_digit(c: c_int) -> bool { + c >= b'0' as c_int && c <= b'9' as c_int +} + +#[unsafe(no_mangle)] +pub extern "C" fn isalpha(c: c_int) -> c_int { + (is_lower(c) || is_upper(c)) as c_int +} + +#[unsafe(no_mangle)] +pub extern "C" fn isupper(c: c_int) -> c_int { + is_upper(c) as c_int +} + +#[unsafe(no_mangle)] +pub extern "C" fn isxdigit(c: c_int) -> c_int { + (is_digit(c) + || (c >= b'a' as c_int && c <= b'f' as c_int) + || (c >= b'A' as c_int && c <= b'F' as c_int)) as c_int +} + +#[unsafe(no_mangle)] +pub extern "C" fn isspace(c: c_int) -> c_int { + matches!( + c, + x if x == b' ' as c_int + || x == b'\t' as c_int + || x == b'\n' as c_int + || x == b'\r' as c_int + || x == 0x0c + || x == 0x0b + ) as c_int +} + +#[unsafe(no_mangle)] +pub extern "C" fn tolower(c: c_int) -> c_int { + if is_upper(c) { + c + (b'a' - b'A') as c_int + } else { + c + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn iswpunct(wc: u32) -> c_int { + matches!(wc, 0x21..=0x2f | 0x3a..=0x40 | 0x5b..=0x60 | 0x7b..=0x7e) as c_int +} From 4a8b86f84498113edff59736192f99e3c9b65fc0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 13:20:57 -0500 Subject: [PATCH 02/15] minimize breaking changes --- Cargo.toml | 3 + README.md | 12 +- build.rs | 19 ++ code-editor/Cargo.toml | 2 +- code-editor/src/lib.rs | 2 +- docsite/Cargo.toml | 2 +- docsite/snippets/runtime.rs | 2 +- docsite/src/main.rs | 8 +- examples/basic/src/main.rs | 2 +- examples/editor/Cargo.toml | 2 +- examples/live-input/src/main.rs | 2 +- src/advanced.rs | 298 +++++++++++++++++++++----------- src/language.rs | 87 +++++----- src/lib.rs | 134 +++++++++----- 14 files changed, 373 insertions(+), 202 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aba2155..23b6282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ macro = ["dep:dioxus-code-macro", "dioxus-code-macro/lang-rust"] runtime = [ "arborium/lang-rust", "dep:arborium-tree-sitter", +] +detection = [ + "runtime", "dep:betlang", ] all-languages = [ diff --git a/README.md b/README.md index 14de30a..de0e583 100644 --- a/README.md +++ b/README.md @@ -55,25 +55,27 @@ For editor-style use cases where the source isn't known at compile time: ```toml [dependencies] -dioxus-code = { version = "0.1", features = ["runtime"] } +dioxus-code = { version = "0.1", features = ["runtime", "detection"] } ``` ```rust +# #[cfg(feature = "detection")] +# { # use dioxus::prelude::*; -use dioxus_code::{Code, CodeOptions, Language, SourceCode, Theme}; +use dioxus_code::{Code, Language, SourceCode, Theme}; # let user_input = String::new(); # let _ = rsx! { Code { - src: SourceCode::new(user_input) - .with_options(CodeOptions::builder().with_language(Language::Rust)), + src: SourceCode::builder(user_input).with_language(Language::Auto), theme: Theme::GITHUB_LIGHT, } } # ; +# } ``` -Language can be set explicitly with the same [`CodeOptions`] builder used by [`code!`], or auto-detected from the source. The default `runtime` feature includes Rust; pass `lang-python`, `lang-toml`, or `all-languages` for the rest. +`Language::Auto` is available only with the `detection` feature. Without detection, pass a concrete language such as `Language::Rust`. The default `runtime` feature includes Rust; pass `lang-python`, `lang-toml`, or `all-languages` for the rest. ## Editor diff --git a/build.rs b/build.rs index 700cfdd..ea0523e 100644 --- a/build.rs +++ b/build.rs @@ -104,6 +104,25 @@ fn main() { )); } + generated.push_str( + r#" /// Every syntax theme, in declaration order. + /// + /// ```rust + /// use dioxus_code::Theme; + /// assert!(Theme::ALL.contains(&Theme::TOKYO_NIGHT)); + /// ``` + pub const ALL: &'static [Theme] = &[ +"#, + ); + for theme in &themes { + generated.push_str(&format!(" Self::{},\n", theme.const_name)); + } + generated.push_str( + r#" ]; + +"#, + ); + generated.push_str( r#"} "#, diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 27a4fcc..6895fe2 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -17,7 +17,7 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } -dioxus-code = { workspace = true, features = ["lang-toml", "lang-python"] } +dioxus-code = { workspace = true, features = ["detection", "lang-toml", "lang-python"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2" diff --git a/code-editor/src/lib.rs b/code-editor/src/lib.rs index 497bbc3..18b1795 100644 --- a/code-editor/src/lib.rs +++ b/code-editor/src/lib.rs @@ -128,7 +128,7 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { } buffer.highlighted() } - None => SourceCode::new(props.value.clone()) + None => SourceCode::builder(props.value.clone()) .with_language(props.language) .into(), } diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index 4b44fdb..835fb43 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] dioxus = { version = "0.7.0", features = ["router"] } -dioxus-code = { workspace = true, features = ["all-languages"] } +dioxus-code = { workspace = true, features = ["all-languages", "detection"] } dioxus-code-editor = { workspace = true } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } diff --git a/docsite/snippets/runtime.rs b/docsite/snippets/runtime.rs index 4ef1257..4dafd65 100644 --- a/docsite/snippets/runtime.rs +++ b/docsite/snippets/runtime.rs @@ -11,7 +11,7 @@ fn App() -> Element { rsx! { Code { - src: SourceCode::new(source()).with_language(Language::Rust), + src: SourceCode::builder(source()).with_language(Language::Rust), theme: CodeTheme::system(Theme::GITHUB_LIGHT, Theme::GITHUB_DARK), } } diff --git a/docsite/src/main.rs b/docsite/src/main.rs index 89861b5..ab65678 100644 --- a/docsite/src/main.rs +++ b/docsite/src/main.rs @@ -544,7 +544,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { "." } p { class: "hero-lede", - "A drop-in component with two source modes — compile-time macro and runtime detection. No JS, no flash of unstyled code." + "A drop-in component with two source modes: compile-time macro and runtime highlighting with explicit language selection." } div { class: "hero-terminal-block", div { class: "hero-terminal-bar", @@ -577,7 +577,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { span { "{theme_label}" } } div { class: "card-code-body", - Code { src: SourceCode::new(HERO_COUNTER).with_language(Language::Rust), theme } + Code { src: SourceCode::builder(HERO_COUNTER).with_language(Language::Rust), theme } } } } @@ -663,7 +663,7 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-aside-num", "02" } div { h3 { class: "receipt-aside-title", "SourceCode" } - p { class: "receipt-aside-text", "Pull it in when input is dynamic. Tree-sitter grammars detect language automatically." } + p { class: "receipt-aside-text", "Pull it in when input is dynamic. Pass the language you want to highlight." } } } div { class: "receipt-aside-row", @@ -785,7 +785,7 @@ fn Docs(scheme: Scheme) -> Element { } div { class: "card-code-body", Code { - src: SourceCode::new(step.code).with_language(step.language), + src: SourceCode::builder(step.code).with_language(step.language), theme, } } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index f002a70..ccaeca0 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -15,7 +15,7 @@ fn App() -> Element { theme: Theme::RUSTDOC_AYU, } Code { - src: SourceCode::new("fn main() {\n println!(\"runtime\");\n}") + src: SourceCode::builder("fn main() {\n println!(\"runtime\");\n}") .with_language(Language::Rust), theme: Theme::GITHUB_LIGHT, } diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 5533d59..8e8b970 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] dioxus = { version = "0.7.0" } -dioxus-code = { workspace = true, features = ["all-languages"] } +dioxus-code = { workspace = true, features = ["all-languages", "detection"] } dioxus-code-editor = { workspace = true } [features] diff --git a/examples/live-input/src/main.rs b/examples/live-input/src/main.rs index adc3fbd..d404c9a 100644 --- a/examples/live-input/src/main.rs +++ b/examples/live-input/src/main.rs @@ -36,7 +36,7 @@ fn App() -> Element { span { "runtime" } } Code { - src: SourceCode::new(source()).with_language(Language::Rust), + src: SourceCode::builder(source()).with_language(Language::Rust), theme: Theme::TOKYO_NIGHT, } } diff --git a/src/advanced.rs b/src/advanced.rs index b9ab4d2..b76767a 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -22,7 +22,8 @@ use std::{borrow::Cow, fmt, ops::Range}; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum HighlightError { - /// No language could be detected and no explicit language was supplied. + /// [`Language::Auto`] could not detect a supported language. + #[cfg(feature = "detection")] LanguageDetectionFailed, /// The tree-sitter parser rejected the selected grammar. GrammarLoad { @@ -51,6 +52,22 @@ pub enum HighlightError { /// The language being parsed. language: Language, }, + /// [`Buffer::edit`] received offsets that are out of bounds or not on + /// UTF-8 character boundaries. + /// + /// The buffer's state is unchanged when this error is returned. + InvalidEdit { + /// First byte the caller said changed. + start_byte: usize, + /// One past the last byte of the replaced region in the previous source. + old_end_byte: usize, + /// One past the last byte of the inserted region in the new source. + new_end_byte: usize, + /// Length of the previous source, for context. + old_len: usize, + /// Length of the new source, for context. + new_len: usize, + }, } impl HighlightError { @@ -78,6 +95,7 @@ impl HighlightError { impl fmt::Display for HighlightError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + #[cfg(feature = "detection")] Self::LanguageDetectionFailed => write!(f, "could not detect language"), Self::GrammarLoad { language, message } => { write!( @@ -108,6 +126,19 @@ impl fmt::Display for HighlightError { Self::Parse { language } => { write!(f, "tree-sitter parse failed for {}", language.slug()) } + Self::InvalidEdit { + start_byte, + old_end_byte, + new_end_byte, + old_len, + new_len, + } => { + write!( + f, + "invalid edit: start_byte {start_byte}, old_end_byte {old_end_byte}, \ + new_end_byte {new_end_byte} (old_len {old_len}, new_len {new_len})" + ) + } } } } @@ -175,7 +206,7 @@ impl From for HighlightQueryErrorKind { #[derive(Debug, Clone, PartialEq, Eq)] pub struct HighlightedSource { source: Cow<'static, str>, - language: Option, + language: Language, spans: Cow<'static, [HighlightSpan]>, } @@ -183,7 +214,7 @@ impl HighlightedSource { #[cfg(feature = "runtime")] pub(crate) fn from_owned_parts( source: String, - language: Option, + language: Language, spans: Vec, ) -> Self { Self { @@ -201,7 +232,7 @@ impl HighlightedSource { /// use dioxus_code::Language; /// use dioxus_code::advanced::HighlightedSource; /// let src = HighlightedSource::from_static_parts("let x = 1;", Language::Rust, &[]); - /// assert_eq!(src.language(), Some(Language::Rust)); + /// assert_eq!(src.language(), Language::Rust); /// ``` pub const fn from_static_parts( source: &'static str, @@ -210,16 +241,13 @@ impl HighlightedSource { ) -> Self { Self { source: Cow::Borrowed(source), - language: Some(language), + language, spans: Cow::Borrowed(spans), } } #[cfg(feature = "runtime")] - pub(crate) fn plaintext( - source: impl Into>, - language: Option, - ) -> Self { + pub(crate) fn plaintext(source: impl Into>, language: Language) -> Self { Self { source: source.into(), language, @@ -245,9 +273,9 @@ impl HighlightedSource { /// use dioxus_code::Language; /// use dioxus_code::advanced::HighlightedSource; /// let src = HighlightedSource::from_static_parts("", Language::Rust, &[]); - /// assert_eq!(src.language(), Some(Language::Rust)); + /// assert_eq!(src.language(), Language::Rust); /// ``` - pub const fn language(&self) -> Option { + pub const fn language(&self) -> Language { self.language } @@ -557,7 +585,7 @@ pub fn TokenSpan(props: TokenSpanProps) -> Element { /// a single coherent unit. [`edit`](Self::edit) applies an incremental edit /// (reusing the cached parse tree); [`replace`](Self::replace) swaps the source /// wholesale; [`set_language`](Self::set_language) switches grammars and -/// reparses. After any mutation, [`source`](Self::source), +/// reparses. After any successful mutation, [`source`](Self::source), /// [`spans`](Self::spans), and [`lines`](Self::lines) reflect the new state. /// /// Available with the `runtime` feature. Hold one per editor instance (e.g. @@ -577,8 +605,8 @@ pub struct Buffer { parser: arborium_tree_sitter::Parser, cursor: arborium_tree_sitter::QueryCursor, language: Language, - incremental: Option, - tree: Option, + incremental: IncrementalGrammar, + tree: arborium_tree_sitter::Tree, source: String, spans: Vec, } @@ -599,19 +627,29 @@ impl Buffer { /// let buffer = Buffer::new(Language::Rust, "fn main() {}").expect("rust grammar loads"); /// assert_eq!(buffer.source(), "fn main() {}"); /// ``` - pub fn new(language: Language, source: impl Into) -> Result { - let mut buffer = Self { - parser: arborium_tree_sitter::Parser::new(), - cursor: arborium_tree_sitter::QueryCursor::new(), + pub fn new(language: Language, source: impl ToString) -> Result { + let source = source.to_string(); + let language = resolve_language(language, &source)?; + let (mut parser, incremental) = Self::parser_for(language)?; + let mut cursor = arborium_tree_sitter::QueryCursor::new(); + let (tree, spans) = Self::parse_source( language, - incremental: None, - tree: None, - source: String::new(), - spans: Vec::new(), - }; - buffer.install_grammar(language)?; - buffer.replace(source)?; - Ok(buffer) + &mut parser, + &incremental.query, + &mut cursor, + &source, + None, + )?; + + Ok(Self { + parser, + cursor, + language, + incremental, + tree, + source, + spans, + }) } /// Replace the source wholesale and reparse from scratch. @@ -627,10 +665,21 @@ impl Buffer { /// buffer.replace("fn new() {}").expect("rust parses"); /// assert_eq!(buffer.source(), "fn new() {}"); /// ``` - pub fn replace(&mut self, source: impl Into) -> Result<(), HighlightError> { - self.source = source.into(); - self.tree = None; - self.reparse() + pub fn replace(&mut self, source: impl ToString) -> Result<(), HighlightError> { + let source = source.to_string(); + let (tree, spans) = Self::parse_source( + self.language, + &mut self.parser, + &self.incremental.query, + &mut self.cursor, + &source, + None, + )?; + + self.source = source; + self.tree = tree; + self.spans = spans; + Ok(()) } /// Apply an incremental edit and reparse, reusing the cached parse tree. @@ -638,8 +687,10 @@ impl Buffer { /// `new_source` must be the full text *after* the edit. `edit` describes /// the byte range that changed — its `start_byte` / `old_end_byte` index /// into the buffer's previous source, and `new_end_byte` indexes into - /// `new_source`. If the edit is malformed, the cached tree is dropped and - /// the new source is parsed from scratch. + /// `new_source`. If the edit is malformed, [`HighlightError::InvalidEdit`] + /// is returned and the buffer is left unchanged. Validation is limited to + /// bounds and UTF-8 character boundaries; callers are responsible for + /// passing an edit range that matches the unchanged prefix and suffix. /// /// ```rust /// use dioxus_code::Language; @@ -654,19 +705,26 @@ impl Buffer { pub fn edit( &mut self, edit: SourceEdit, - new_source: impl Into, + new_source: impl ToString, ) -> Result<(), HighlightError> { - let new_source: String = new_source.into(); - if let (Some(_), Some(tree)) = (&self.incremental, self.tree.as_mut()) { - match edit.into_input_edit(&self.source, &new_source) { - Some(input_edit) => tree.edit(&input_edit), - None => self.tree = None, - } - } else { - self.tree = None; - } + let new_source: String = new_source.to_string(); + let input_edit = edit.into_input_edit(&self.source, &new_source)?; + + let mut old_tree = self.tree.clone(); + old_tree.edit(&input_edit); + let (tree, spans) = Self::parse_source( + self.language, + &mut self.parser, + &self.incremental.query, + &mut self.cursor, + &new_source, + Some(&old_tree), + )?; + self.source = new_source; - self.reparse() + self.tree = tree; + self.spans = spans; + Ok(()) } /// Switch grammars and reparse the current source. @@ -681,12 +739,26 @@ impl Buffer { /// assert_eq!(buffer.language(), Language::Rust); /// ``` pub fn set_language(&mut self, language: Language) -> Result<(), HighlightError> { + let language = resolve_language(language, &self.source)?; if self.language == language { return Ok(()); } - self.install_grammar(language)?; - self.tree = None; - self.reparse() + let (mut parser, incremental) = Self::parser_for(language)?; + let (tree, spans) = Self::parse_source( + language, + &mut parser, + &incremental.query, + &mut self.cursor, + &self.source, + None, + )?; + + self.parser = parser; + self.incremental = incremental; + self.language = language; + self.tree = tree; + self.spans = spans; + Ok(()) } /// The current source text. @@ -719,56 +791,39 @@ impl Buffer { /// Useful for handing off to [`Code()`](crate::Code()) or any consumer /// that takes the frozen snapshot type. pub fn highlighted(&self) -> HighlightedSource { - HighlightedSource::from_owned_parts( - self.source.clone(), - Some(self.language), - self.spans.clone(), - ) + HighlightedSource::from_owned_parts(self.source.clone(), self.language, self.spans.clone()) } - fn install_grammar(&mut self, language: Language) -> Result<(), HighlightError> { - self.language = language; + fn parser_for( + language: Language, + ) -> Result<(arborium_tree_sitter::Parser, IncrementalGrammar), HighlightError> { + let mut parser = arborium_tree_sitter::Parser::new(); let (language_fn, highlights_query) = grammar_for(language); let ts_language: arborium_tree_sitter::Language = language_fn.into(); - if let Err(error) = self.parser.set_language(&ts_language) { - return self.fail(HighlightError::grammar_load(language, error)); + if let Err(error) = parser.set_language(&ts_language) { + return Err(HighlightError::grammar_load(language, error)); } match arborium_tree_sitter::Query::new(&ts_language, highlights_query) { - Ok(query) => { - self.incremental = Some(IncrementalGrammar { query }); - Ok(()) - } - Err(error) => self.fail(HighlightError::query(language, error)), + Ok(query) => Ok((parser, IncrementalGrammar { query })), + Err(error) => Err(HighlightError::query(language, error)), } } - fn fail(&mut self, error: HighlightError) -> Result { - self.incremental = None; - self.tree = None; - self.spans.clear(); - Err(error) - } - - fn reparse(&mut self) -> Result<(), HighlightError> { - let Some(grammar) = &self.incremental else { - self.tree = None; - self.spans.clear(); - return Err(HighlightError::GrammarLoad { - language: self.language, - message: "grammar is not loaded".to_owned(), - }); - }; - - match self.parser.parse(&self.source, self.tree.as_ref()) { + fn parse_source( + language: Language, + parser: &mut arborium_tree_sitter::Parser, + query: &arborium_tree_sitter::Query, + cursor: &mut arborium_tree_sitter::QueryCursor, + source: &str, + old_tree: Option<&arborium_tree_sitter::Tree>, + ) -> Result<(arborium_tree_sitter::Tree, Vec), HighlightError> { + match parser.parse(source, old_tree) { Some(tree) => { - self.spans = collect_spans(&grammar.query, &mut self.cursor, &tree, &self.source); - self.tree = Some(tree); - Ok(()) + let spans = collect_spans(query, cursor, &tree, source); + Ok((tree, spans)) } - None => self.fail(HighlightError::Parse { - language: self.language, - }), + None => Err(HighlightError::Parse { language }), } } } @@ -805,11 +860,23 @@ fn collect_spans( normalize_spans(raw) } +#[cfg(feature = "runtime")] +fn resolve_language(language: Language, _source: &str) -> Result { + #[cfg(feature = "detection")] + if language == Language::Auto { + return Language::detect(_source).ok_or(HighlightError::LanguageDetectionFailed); + } + + Ok(language) +} + #[cfg(feature = "runtime")] fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'static str) { // Rust is bundled with the `runtime` feature; everything else is opt-in via // its `lang-*` cargo feature (or the `all-languages` umbrella). match language { + #[cfg(feature = "detection")] + Language::Auto => unreachable!("auto language must be resolved before loading a grammar"), Language::Rust => ( arborium::lang_rust::language(), arborium::lang_rust::HIGHLIGHTS_QUERY, @@ -1329,8 +1396,8 @@ fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'stati /// A byte-range edit description used to drive incremental highlighting. /// -/// Build one from a real edit signal (for example a contenteditable -/// `beforeinput` event) and pass it to [`Buffer::edit`]. `start_byte` and +/// Build one from a real edit signal (for example a textarea `beforeinput` +/// event) and pass it to [`Buffer::edit`]. `start_byte` and /// `old_end_byte` index into the buffer's previous source, while /// `new_end_byte` indexes into the new source supplied alongside the edit. /// @@ -1357,18 +1424,26 @@ impl SourceEdit { self, old_source: &str, new_source: &str, - ) -> Option { + ) -> Result { if self.start_byte > self.old_end_byte || self.start_byte > self.new_end_byte || self.old_end_byte > old_source.len() || self.new_end_byte > new_source.len() || !old_source.is_char_boundary(self.start_byte) || !old_source.is_char_boundary(self.old_end_byte) + || !new_source.is_char_boundary(self.start_byte) || !new_source.is_char_boundary(self.new_end_byte) { - return None; + return Err(HighlightError::InvalidEdit { + start_byte: self.start_byte, + old_end_byte: self.old_end_byte, + new_end_byte: self.new_end_byte, + old_len: old_source.len(), + new_len: new_source.len(), + }); } - Some(arborium_tree_sitter::InputEdit { + + Ok(arborium_tree_sitter::InputEdit { start_byte: self.start_byte, old_end_byte: self.old_end_byte, new_end_byte: self.new_end_byte, @@ -1443,9 +1518,7 @@ mod buffer_tests { } fn batch_spans(source: &str, language: Language) -> Vec { - let snapshot: HighlightedSource = SourceCode::new(source.to_owned()) - .with_language(language) - .into(); + let snapshot: HighlightedSource = SourceCode::new(language, source.to_owned()).into(); snapshot.spans().to_vec() } @@ -1484,11 +1557,13 @@ mod buffer_tests { } #[test] - fn malformed_edit_falls_back_to_full_parse() { + fn malformed_edit_returns_typed_error_and_leaves_state_unchanged() { let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap(); + let previous_spans = buffer.spans().to_vec(); let updated = "fn main() { let x = 12; }"; - buffer - .edit( + + assert_eq!( + buffer.edit( // old_end_byte beyond the previous source — must not panic. SourceEdit { start_byte: 21, @@ -1496,13 +1571,38 @@ mod buffer_tests { new_end_byte: 22, }, updated, + ), + Err(HighlightError::InvalidEdit { + start_byte: 21, + old_end_byte: 999, + new_end_byte: 22, + old_len: "fn main() { let x = 1; }".len(), + new_len: updated.len(), + }), + ); + assert_eq!(buffer.source(), "fn main() { let x = 1; }"); + assert_eq!(buffer.spans(), previous_spans.as_slice()); + } + + #[test] + fn semantic_edit_mismatch_is_not_validated() { + let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap(); + let updated = "fn main() { let y = 1; }"; + + buffer + .edit( + // The unchanged suffix does not match this edit; validation + // intentionally stays O(1) and trusts callers on semantics. + SourceEdit { + start_byte: 21, + old_end_byte: 21, + new_end_byte: 22, + }, + updated, ) .unwrap(); - assert_eq!( - span_ranges(buffer.spans()), - span_ranges(&batch_spans(updated, Language::Rust)), - ); + assert_eq!(buffer.source(), updated); } #[test] diff --git a/src/language.rs b/src/language.rs index 25122b3..f5c1e64 100644 --- a/src/language.rs +++ b/src/language.rs @@ -1,11 +1,9 @@ //! Tree-sitter grammar selection. //! -//! [`Language`] is a closed set of Arborium language slugs. Each named variant -//! is gated by the same cargo feature as the corresponding grammar in -//! `dioxus-code`, so the enum only ever exposes variants whose grammar is -//! actually compiled into this build. - -use dioxus::core::SuperFrom; +//! [`Language`] is a closed set of language slugs. Grammar variants are gated +//! by the same cargo feature as the corresponding grammar in `dioxus-code`, so +//! the enum only ever exposes variants whose grammar is actually compiled into +//! this build. [`Language::Auto`] is available with the `detection` feature. macro_rules! define_languages { ( @@ -16,10 +14,11 @@ macro_rules! define_languages { ) => { /// Tree-sitter grammar identifier. /// - /// Each variant maps to an Arborium language slug via - /// [`Language::slug`]. Variants are gated by the same `lang-*` cargo - /// features as the grammar lookup table, so unsupported builds simply - /// don't expose those variants. + /// Each variant maps to a language slug via [`Language::slug`]. + /// Grammar variants are gated by the same `lang-*` cargo features as + /// the grammar lookup table, so unsupported builds simply don't expose + /// those variants. [`Language::Auto`] is available with the + /// `detection` feature. /// /// ```rust /// use dioxus_code::Language; @@ -29,9 +28,12 @@ macro_rules! define_languages { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Language { + /// Automatically detect the language from the source text. + #[cfg(feature = "detection")] + Auto, $( $(#[$attr])* - #[doc = concat!("Arborium slug `\"", $slug, "\"`.")] + #[doc = concat!("Language slug `\"", $slug, "\"`.")] $variant, )* } @@ -47,15 +49,19 @@ macro_rules! define_languages { /// assert!(Language::ALL.contains(&Language::Rust)); /// ``` pub const ALL: &'static [Language] = &[ + #[cfg(feature = "detection")] + Self::Auto, $( $(#[$attr])* Self::$variant, )* ]; - /// Arborium slug for this language. + /// Stable slug for this language. pub const fn slug(self) -> &'static str { match self { + #[cfg(feature = "detection")] + Self::Auto => "auto", $( $(#[$attr])* Self::$variant => $slug, @@ -63,12 +69,14 @@ macro_rules! define_languages { } } - /// Parse an Arborium slug into a [`Language`]. + /// Parse a language slug into a [`Language`]. /// /// Returns `None` for unknown slugs and for slugs whose grammar /// feature is disabled in this build. pub fn from_slug(slug: &str) -> Option { match slug { + #[cfg(feature = "detection")] + "auto" => Some(Self::Auto), $( $(#[$attr])* $slug => Some(Self::$variant), @@ -83,23 +91,40 @@ macro_rules! define_languages { impl Language { /// Best-effort detection from a path, filename, shebang, or file contents. /// - /// First uses Arborium's path and shebang detector, then falls back to - /// betlang source-language inference from source text. The - /// detected slug is mapped into a [`Language`] variant, returning `None` - /// when detection fails or the detected language's grammar feature is - /// disabled in this build. + /// Uses Arborium's path and shebang detector. With the `detection` feature + /// enabled, this also falls back to betlang source-language inference from + /// source text. The detected slug is mapped into a [`Language`] variant, + /// returning `None` when detection fails or the detected language's grammar + /// feature is disabled in this build. /// /// Available with the `runtime` feature. #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] pub fn detect(input: &str) -> Option { - arborium::detect_language(input) - .and_then(Self::from_slug) - .or_else(|| { + let detected = arborium::detect_language(input).and_then(Self::from_detected_slug); + + #[cfg(feature = "detection")] + { + detected.or_else(|| { betlang::detect(input) .language() - .and_then(|language| Self::from_slug(language.slug())) + .and_then(|language| Self::from_detected_slug(language.slug())) }) + } + + #[cfg(not(feature = "detection"))] + { + detected + } + } + + #[cfg(feature = "runtime")] + fn from_detected_slug(slug: &str) -> Option { + match Self::from_slug(slug) { + #[cfg(feature = "detection")] + Some(Self::Auto) => None, + language => language, + } } } @@ -310,21 +335,3 @@ define_languages! { #[cfg(feature = "lang-zsh")] Zsh => "zsh", } - -#[doc(hidden)] -pub struct OptionLanguageFromStrMarker; - -impl<'a> SuperFrom<&'a str, OptionLanguageFromStrMarker> for Option { - fn super_from(slug: &'a str) -> Self { - Language::from_slug(slug) - } -} - -#[doc(hidden)] -pub struct OptionLanguageFromStringMarker; - -impl SuperFrom for Option { - fn super_from(slug: String) -> Self { - Language::from_slug(&slug) - } -} diff --git a/src/lib.rs b/src/lib.rs index 1cd3c85..cc048dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,14 +20,13 @@ const CODE_CSS: Asset = asset!("/assets/dioxus-code.css"); #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] pub use dioxus_code_macro::{code, code_str}; -/// Options shared by the [`code!`] and [`code_str!`] macros and runtime -/// [`SourceCode`]. +/// Compile-time options for the [`code!`] and [`code_str!`] macros. /// -/// The macros read this builder syntactically at compile time; pass +/// Both macros read this builder syntactically; pass /// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to override the /// language that would otherwise be inferred from the file extension. For -/// [`code_str!`] the language is required since there is no extension to infer -/// from. [`SourceCode`] consumes the same builder at runtime. +/// [`code_str!`] the language is required since there is no extension to +/// infer from. /// /// ```rust /// use dioxus_code::{CodeOptions, Language, code}; @@ -219,60 +218,72 @@ pub use advanced::{HighlightError, HighlightQueryErrorKind}; /// Source text to highlight at runtime. /// /// Available with the `runtime` feature. Build one with [`SourceCode::new`], -/// optionally annotate it with [`SourceCode::with_language`], then pass it to -/// [`Code()`]. +/// or the source-first [`SourceCode::builder`], then pass it to [`Code()`]. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; -/// let _src = SourceCode::new("fn main() {}").with_language(Language::Rust); +/// let _src = SourceCode::new(Language::Rust, "fn main() {}"); /// ``` #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct SourceCode { source: String, - options: CodeOptions, + language: Language, +} + +/// Source-first builder for [`SourceCode`]. +/// +/// ```rust +/// use dioxus_code::{Language, SourceCode}; +/// let _src = SourceCode::builder("fn main() {}").with_language(Language::Rust); +/// ``` +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceCodeBuilder { + source: String, } #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] impl SourceCode { - /// Wrap a raw source string with no language hint. + /// Wrap a raw source string with an explicit language. /// /// ```rust - /// use dioxus_code::SourceCode; - /// let _src = SourceCode::new("fn main() {}"); + /// use dioxus_code::{Language, SourceCode}; + /// let _src = SourceCode::new(Language::Rust, "fn main() {}"); /// ``` - pub fn new(source: impl Into) -> Self { + pub fn new(language: Language, source: impl ToString) -> Self { Self { - source: source.into(), - options: CodeOptions::new(), + source: source.to_string(), + language, } } - /// Apply shared [`CodeOptions`]. + /// Start a source-first builder. /// /// ```rust - /// use dioxus_code::{CodeOptions, Language, SourceCode}; - /// let options = CodeOptions::builder().with_language(Language::Rust); - /// let _src = SourceCode::new("fn main() {}").with_options(options); + /// use dioxus_code::{Language, SourceCode}; + /// let _src = SourceCode::builder("fn main() {}").with_language(Language::Rust); /// ``` - pub fn with_options(mut self, options: CodeOptions) -> Self { - self.options = options; - self + pub fn builder(source: impl ToString) -> SourceCodeBuilder { + SourceCodeBuilder { + source: source.to_string(), + } } - /// Set the language explicitly. + /// Replace the language used to highlight this source. /// /// To set the language from a runtime slug, use [`Language::from_slug`] /// and pass the resulting variant. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; - /// let _src = SourceCode::new("fn main() {}").with_language(Language::Rust); + /// let _src = SourceCode::new(Language::Rust, "fn main() {}").with_language(Language::Rust); /// ``` - pub fn with_language(mut self, language: impl Into>) -> Self { - self.options = self.options.with_language(language); + pub fn with_language(mut self, language: Language) -> Self { + self.language = language; self } @@ -281,21 +292,12 @@ impl SourceCode { /// Use `Into` for the lossy rendering path that discards /// the error and renders plaintext. pub fn highlight(self) -> Result { - let language = self - .options - .language() - .or_else(|| Language::detect(&self.source)); - match language { - Some(language) => { - advanced::Buffer::new(language, self.source).map(|buffer| buffer.highlighted()) - } - None => Err(HighlightError::LanguageDetectionFailed), - } + advanced::Buffer::new(self.language, self.source).map(|buffer| buffer.highlighted()) } fn highlight_or_plaintext(self) -> advanced::HighlightedSource { + let language = self.language; let source = self.source.clone(); - let language = self.options.language(); match self.highlight() { Ok(source) => source, Err(_) => advanced::HighlightedSource::plaintext(source, language), @@ -303,6 +305,20 @@ impl SourceCode { } } +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +impl SourceCodeBuilder { + /// Finish the builder with an explicit language. + /// + /// ```rust + /// use dioxus_code::{Language, SourceCode}; + /// let _src = SourceCode::builder("fn main() {}").with_language(Language::Rust); + /// ``` + pub fn with_language(self, language: Language) -> SourceCode { + SourceCode::new(language, self.source) + } +} + #[cfg(feature = "runtime")] pub(crate) struct RawHighlightSpan { pub(crate) start: u32, @@ -408,7 +424,7 @@ pub fn Code(props: CodeProps) -> Element { let source = &props.src; let segments = source.trimmed_segments(); let class = format!("dxc {}", props.theme.classes()); - let language = source.language().map(Language::slug).unwrap_or("text"); + let language = source.language().slug(); rsx! { advanced::CodeThemeStyles { theme: props.theme } @@ -494,11 +510,22 @@ mod tests { #[cfg(feature = "runtime")] #[test] - fn runtime_code_options_highlights() { - let tree: advanced::HighlightedSource = SourceCode::new("fn main() {}") - .with_options(CodeOptions::builder().with_language(Language::Rust)) + fn runtime_source_code_highlights() { + let tree: advanced::HighlightedSource = + SourceCode::new(Language::Rust, "fn main() {}").into(); + assert_eq!(tree.language(), Language::Rust); + assert!(tree.spans().iter().any(|span| { + span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" + })); + } + + #[cfg(feature = "runtime")] + #[test] + fn runtime_source_code_builder_highlights() { + let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}") + .with_language(Language::Rust) .into(); - assert_eq!(tree.language(), Some(Language::Rust)); + assert_eq!(tree.language(), Language::Rust); assert!(tree.spans().iter().any(|span| { span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" })); @@ -506,15 +533,28 @@ mod tests { #[cfg(feature = "runtime")] #[test] - fn runtime_raw_string_detects_language_from_source() { - let tree: advanced::HighlightedSource = - SourceCode::new("use std::fmt;\nfn main() { println!(\"hi\"); }").into(); - assert_eq!(tree.language(), Some(Language::Rust)); + fn language_detect_remains_available_with_runtime() { + assert_eq!(Language::detect("demo.rs"), Some(Language::Rust)); + } + + #[cfg(feature = "detection")] + #[test] + fn runtime_source_code_auto_language_highlights_detected_source() { + let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}") + .with_language(Language::Auto) + .into(); + assert_eq!(tree.language(), Language::Rust); assert!(tree.spans().iter().any(|span| { span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" })); } + #[cfg(feature = "detection")] + #[test] + fn auto_language_is_available_when_detection_enabled() { + assert!(Language::ALL.contains(&Language::Auto)); + } + #[cfg(feature = "macro")] #[test] fn code_str_macro_highlights_inline_source() { @@ -522,7 +562,7 @@ mod tests { "fn main() {}", CodeOptions::builder().with_language(Language::Rust) ); - assert_eq!(TREE.language(), Some(Language::Rust)); + assert_eq!(TREE.language(), Language::Rust); assert_eq!(TREE.source(), "fn main() {}"); assert!(TREE.spans().iter().any(|span| { span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn" From 63f9b197cb88ce7fe205817f6423f1d437ea1fd6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 13:52:51 -0500 Subject: [PATCH 03/15] shrink diff --- .github/workflows/main.yml | 8 +-- .github/workflows/nightly.yml | 6 +- Cargo.toml | 13 +---- README.md | 19 +++---- code-editor/Cargo.toml | 6 +- code-editor/README.md | 2 +- code-editor/src/lib.rs | 40 +++++++++----- code-editor/src/main.rs | 38 ------------- dioxus-code-macro/README.md | 2 +- docsite/Cargo.toml | 2 +- docsite/assets/app.css | 1 - docsite/snippets/runtime.rs | 2 +- docsite/src/components/card/style.css | 2 - docsite/src/main.rs | 26 ++++----- examples/basic/src/main.rs | 3 +- examples/editor/Cargo.toml | 6 +- examples/editor/src/main.rs | 10 ++-- examples/live-input/src/main.rs | 2 +- src/advanced.rs | 19 ------- src/language.rs | 52 +++++------------ src/lib.rs | 80 +-------------------------- src/wasm_ctype.rs | 64 --------------------- 22 files changed, 86 insertions(+), 317 deletions(-) delete mode 100644 code-editor/src/main.rs delete mode 100644 src/wasm_ctype.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 311f170..a49853c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,13 +20,13 @@ jobs: with: all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web test: uses: dioxuslabs/dioxus-ci/.github/workflows/test.yml@v0.1.0 with: no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web fmt: uses: dioxuslabs/dioxus-ci/.github/workflows/fmt.yml@v0.1.0 @@ -36,11 +36,11 @@ jobs: with: all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web clippy: uses: dioxuslabs/dioxus-ci/.github/workflows/clippy.yml@v0.1.0 with: all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index cfd98ed..ef621d3 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -19,14 +19,14 @@ jobs: toolchain: nightly all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web test: uses: dioxuslabs/dioxus-ci/.github/workflows/test.yml@v0.1.0 with: toolchain: nightly no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web clippy: uses: dioxuslabs/dioxus-ci/.github/workflows/clippy.yml@v0.1.0 @@ -34,7 +34,7 @@ jobs: toolchain: nightly all-features: false no-default-features: true - features: dioxus-code/all-languages dioxus-code-editor/web dioxus-code-docsite/web dioxus-code-live-input/web + features: dioxus-code/all-languages dioxus-code-docsite/web dioxus-code-live-input/web web-demo: uses: dioxuslabs/dioxus-ci/.github/workflows/web-build.yml@v0.1.0 diff --git a/Cargo.toml b/Cargo.toml index 23b6282..7fd385c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ categories = ["gui", "web-programming"] [workspace.dependencies] dioxus-code = { version = "0.1.1", path = "." } dioxus-code-editor = { version = "0.1.2", path = "code-editor", default-features = false } -betlang = { version = "0.1.0" } [package] name = "dioxus-code" @@ -44,14 +43,8 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["macro"] macro = ["dep:dioxus-code-macro", "dioxus-code-macro/lang-rust"] -runtime = [ - "arborium/lang-rust", - "dep:arborium-tree-sitter", -] -detection = [ - "runtime", - "dep:betlang", -] +runtime = ["arborium/lang-rust", "dep:arborium-tree-sitter"] +detection = ["runtime", "dep:betlang"] all-languages = [ "runtime", "arborium/all-languages", @@ -268,7 +261,7 @@ arborium-theme = "2.16.0" arborium-tree-sitter = { version = "2.16.0", optional = true } dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } dioxus-code-macro = { version = "0.1.0", path = "dioxus-code-macro", default-features = false, optional = true } -betlang = { workspace = true, optional = true } +betlang = { version = "0.1.0", optional = true } [build-dependencies] arborium = { version = "2.16.0", default-features = false } diff --git a/README.md b/README.md index de0e583..5b37180 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ A small Dioxus component for rendering source code with proper highlighting. Par Two ways to highlight: -- **[`code!`] macro** — parses at compile time. The runtime ships only the spans, no parser. Default. -- **[`SourceCode`]** — parses at runtime. Opt in with the `runtime` feature when the source isn't known until the user types it. +- **[`code!`] macro** — parses at compile time and embeds the highlighted spans. Default. +- **[`SourceCode`]** — parses at runtime. Opt in with the `runtime` feature for dynamic source text. ## Quick start @@ -51,35 +51,32 @@ When the file extension is ambiguous, pass [`CodeOptions::builder`] with [`CodeO ## Runtime highlighting -For editor-style use cases where the source isn't known at compile time: +For editor-style use cases with dynamic source text: ```toml [dependencies] -dioxus-code = { version = "0.1", features = ["runtime", "detection"] } +dioxus-code = { version = "0.1", features = ["runtime"] } ``` ```rust -# #[cfg(feature = "detection")] -# { # use dioxus::prelude::*; use dioxus_code::{Code, Language, SourceCode, Theme}; # let user_input = String::new(); # let _ = rsx! { Code { - src: SourceCode::builder(user_input).with_language(Language::Auto), + src: SourceCode::new(Language::Rust, user_input), theme: Theme::GITHUB_LIGHT, } } # ; -# } ``` -`Language::Auto` is available only with the `detection` feature. Without detection, pass a concrete language such as `Language::Rust`. The default `runtime` feature includes Rust; pass `lang-python`, `lang-toml`, or `all-languages` for the rest. +Pass a [`Language`] variant when building [`SourceCode`]. The `runtime` feature includes Rust; enable the matching `lang-*` feature, or `all-languages`, for additional grammars. ## Editor -[`dioxus-code-editor`] is a sibling crate that pairs the highlighter with a `contenteditable` input layer: +[`dioxus-code-editor`] is a sibling crate that pairs the highlighter with a textarea input layer: ```rust # use dioxus::prelude::*; @@ -133,7 +130,7 @@ Code { ```sh dx serve --example dioxus-code-basic # macro + runtime side by side -dx serve --example dioxus-code-macro-only # compile-time only, no parser in the binary +dx serve --example dioxus-code-macro-only # compile-time highlighted spans dx serve --example dioxus-code-live-input # textarea bound to runtime highlighter ``` diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 6895fe2..2a3505d 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -17,7 +17,7 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } -dioxus-code = { workspace = true, features = ["detection", "lang-toml", "lang-python"] } +dioxus-code = { workspace = true, features = ["runtime"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2" @@ -30,6 +30,4 @@ web-sys = { version = "0.3", features = [ ] } [features] -default = ["desktop"] -desktop = ["dioxus/desktop", "dioxus/launch"] -web = ["dioxus/web", "dioxus/launch"] +all-languages = ["dioxus-code/all-languages"] diff --git a/code-editor/README.md b/code-editor/README.md index d71a2be..d8e59d1 100644 --- a/code-editor/README.md +++ b/code-editor/README.md @@ -53,7 +53,7 @@ The component is controlled — drive [`CodeEditorProps::value`] from your own s | prop | description | |---|---| | [`CodeEditorProps::value`] | Current editor contents. | -| [`CodeEditorProps::language`] | Tree-sitter grammar selection. Pass a [`Language`] variant (for example [`Language::Rust`]) or use [`Language::from_slug`] for custom slugs. | +| [`CodeEditorProps::language`] | Syntax grammar selection. Pass a [`Language`] variant (for example [`Language::Rust`]) or use [`Language::from_slug`] for runtime slugs. | | [`CodeEditorProps::theme`] | Syntax theme selection shared with [`dioxus-code`](https://crates.io/crates/dioxus-code); accepts [`Theme`] or [`CodeTheme`]. | | [`CodeEditorProps::line_numbers`] | Show a one-based line gutter. Defaults to `true`. | | [`CodeEditorProps::read_only`] | Disable editing while preserving highlighting. | diff --git a/code-editor/src/lib.rs b/code-editor/src/lib.rs index 18b1795..7e0e7a2 100644 --- a/code-editor/src/lib.rs +++ b/code-editor/src/lib.rs @@ -5,7 +5,7 @@ use dioxus::prelude::*; pub use dioxus_code::Language; #[cfg(test)] use dioxus_code::Theme; -use dioxus_code::advanced::{Buffer, CodeThemeStyles, TokenSpan}; +use dioxus_code::advanced::{Buffer, CodeThemeStyles, HighlightError, TokenSpan}; #[cfg(test)] use dioxus_code::advanced::{HighlightSegment, HighlightedSource}; use dioxus_code::{CodeTheme, SourceCode}; @@ -71,6 +71,11 @@ pub struct CodeEditorProps { pub oninput: EventHandler, } +struct EditorBuffer { + buffer: Option, + language: Language, +} + /// Editable syntax-highlighted code surface. /// /// The component is controlled by [`CodeEditorProps::value`]; update that value @@ -96,10 +101,15 @@ pub struct CodeEditorProps { /// ``` #[component] pub fn CodeEditor(props: CodeEditorProps) -> Element { - let buffer = use_hook({ + let state = use_hook({ let value = props.value.clone(); let language = props.language; - move || Rc::new(RefCell::new(Buffer::new(language, value).ok())) + move || { + Rc::new(RefCell::new(EditorBuffer { + buffer: Buffer::new(language, value).ok(), + language, + })) + } }); let edit_tracker = use_hook(|| { Rc::new(RefCell::new(edit_capture::InputEditTracker::new( @@ -109,28 +119,30 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { let edit = edit_tracker.borrow_mut().take_for_render(&props.value); let snapshot = { - let mut buffer_slot = buffer.borrow_mut(); - if buffer_slot.is_none() { - *buffer_slot = Buffer::new(props.language, props.value.clone()).ok(); + let mut slot = state.borrow_mut(); + if slot.language != props.language { + slot.buffer = Buffer::new(props.language, props.value.clone()).ok(); + slot.language = props.language; } - match buffer_slot.as_mut() { + match slot.buffer.as_mut() { Some(buffer) => { - if buffer.language() != props.language { - let _ = buffer.set_language(props.language); - } if buffer.source() != props.value { let result = match edit { - Some(edit) => buffer.edit(edit, props.value.clone()), + Some(edit) => match buffer.edit(edit, props.value.clone()) { + Ok(()) => Ok(()), + Err(HighlightError::InvalidEdit { .. }) => { + buffer.replace(props.value.clone()) + } + Err(error) => Err(error), + }, None => buffer.replace(props.value.clone()), }; let _ = result; } buffer.highlighted() } - None => SourceCode::builder(props.value.clone()) - .with_language(props.language) - .into(), + None => SourceCode::new(props.language, props.value.clone()).into(), } }; let lines = snapshot.lines(); diff --git a/code-editor/src/main.rs b/code-editor/src/main.rs deleted file mode 100644 index 7125e48..0000000 --- a/code-editor/src/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -use dioxus::prelude::*; -use dioxus_code::Theme; -use dioxus_code_editor::{CodeEditor, Language}; - -const DEMO_CSS: Asset = asset!("/assets/demo.css"); - -const STARTER: &str = r#"pub fn luminance(rgb: (u8, u8, u8)) -> f32 { - let (r, g, b) = rgb; - 0.2126 * r as f32 + 0.7152 * g as f32 + 0.0722 * b as f32 -} -"#; - -fn main() { - dioxus::launch(App); -} - -#[component] -fn App() -> Element { - let mut source = use_signal(|| STARTER.to_string()); - let language = Language::detect(&source()).unwrap_or(Language::Rust); - let language_label = language.slug(); - - rsx! { - document::Stylesheet { href: DEMO_CSS } - main { class: "shell", - section { class: "toolbar", - h1 { "Code editor component" } - span { "{language_label}" } - } - CodeEditor { - value: source(), - language, - theme: Theme::TOKYO_NIGHT, - oninput: move |value| source.set(value), - } - } - } -} diff --git a/dioxus-code-macro/README.md b/dioxus-code-macro/README.md index d9d55ad..c3e914b 100644 --- a/dioxus-code-macro/README.md +++ b/dioxus-code-macro/README.md @@ -19,7 +19,7 @@ Implementation crate for the [`code!`] macro re-exported by [`dioxus-code`](https://crates.io/crates/dioxus-code) under its default `macro` feature. You usually depend on `dioxus-code` instead of pulling this in directly. -The macro reads a source file at compile time, parses it with [`arborium`](https://crates.io/crates/arborium), and expands to a static span tree. The runtime binary ships only the spans — no parser. +The macro reads a source file at compile time, parses it with [`arborium`](https://crates.io/crates/arborium), and expands to a static span tree that can be rendered by `dioxus-code`. ```rust use dioxus_code::code; diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index 835fb43..c63503c 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] dioxus = { version = "0.7.0", features = ["router"] } -dioxus-code = { workspace = true, features = ["all-languages", "detection"] } +dioxus-code = { workspace = true, features = ["runtime", "lang-toml"] } dioxus-code-editor = { workspace = true } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } diff --git a/docsite/assets/app.css b/docsite/assets/app.css index e80b816..47512b6 100644 --- a/docsite/assets/app.css +++ b/docsite/assets/app.css @@ -325,7 +325,6 @@ a { font-size: 13px; line-height: 1.65; margin: 0; - min-height: 380px; padding: 18px 20px; } diff --git a/docsite/snippets/runtime.rs b/docsite/snippets/runtime.rs index 4dafd65..774300f 100644 --- a/docsite/snippets/runtime.rs +++ b/docsite/snippets/runtime.rs @@ -11,7 +11,7 @@ fn App() -> Element { rsx! { Code { - src: SourceCode::builder(source()).with_language(Language::Rust), + src: SourceCode::new(Language::Rust, source()), theme: CodeTheme::system(Theme::GITHUB_LIGHT, Theme::GITHUB_DARK), } } diff --git a/docsite/src/components/card/style.css b/docsite/src/components/card/style.css index de5cfa7..2bad15f 100644 --- a/docsite/src/components/card/style.css +++ b/docsite/src/components/card/style.css @@ -1,13 +1,11 @@ .card { display: flex; flex-direction: column; - padding: 1.5rem 0; border: 1px solid var(--light, var(--primary-color-6)) var(--dark, var(--primary-color-5)); border-radius: 1rem; background-color: var(--light, var(--primary-color-2)) var(--dark, var(--primary-color-3)); box-shadow: 0 2px 10px rgb(0 0 0 / 10%); color: var(--secondary-color-4); - gap: 1.5rem; } .card-header { diff --git a/docsite/src/main.rs b/docsite/src/main.rs index ab65678..6a08fd0 100644 --- a/docsite/src/main.rs +++ b/docsite/src/main.rs @@ -577,7 +577,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { span { "{theme_label}" } } div { class: "card-code-body", - Code { src: SourceCode::builder(HERO_COUNTER).with_language(Language::Rust), theme } + Code { src: SourceCode::new(Language::Rust, HERO_COUNTER), theme } } } } @@ -640,15 +640,15 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-value", "OPT-IN" } } li { class: "receipt-item receipt-optional", - span { class: "receipt-label", "Tree-sitter grammars" } + span { class: "receipt-label", "Runtime grammars" } span { class: "receipt-dots" } span { class: "receipt-value", "+3.33 MiB" } } } div { class: "receipt-rule double" } div { class: "receipt-total", - span { class: "receipt-total-label", "PARSER BYTES SHIPPED" } - span { class: "receipt-total-value", "0" } + span { class: "receipt-total-label", "COMPILE-TIME MODE" } + span { class: "receipt-total-value", "STATIC" } } } aside { class: "receipt-aside", @@ -656,14 +656,14 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-aside-num", "01" } div { h3 { class: "receipt-aside-title", "code!" } - p { class: "receipt-aside-text", "Tokenizes during cargo build. The runtime gets pre-styled markup with no parser bytes." } + p { class: "receipt-aside-text", "Tokenizes during cargo build and embeds highlighted spans for rendering." } } } div { class: "receipt-aside-row", span { class: "receipt-aside-num", "02" } div { h3 { class: "receipt-aside-title", "SourceCode" } - p { class: "receipt-aside-text", "Pull it in when input is dynamic. Pass the language you want to highlight." } + p { class: "receipt-aside-text", "Pull it in when input is dynamic and pass the language your source uses." } } } div { class: "receipt-aside-row", @@ -690,9 +690,6 @@ fn Playground( let theme_pair = theme_pairs[active_idx()]; let theme = theme_pair.code_theme(scheme); let value = use_memo(move || Some(active_idx())); - let language = Language::detect(&source()).unwrap_or(Language::Rust); - let language_label = language.slug(); - let source_len = source().chars().count(); rsx! { section { id: "playground", class: "section", @@ -704,9 +701,9 @@ fn Playground( div { class: "playground-grid", Card { class: "card-editor", div { class: "card-bar", - span { "source" } + span { "source.rs" } span { class: "editor-meta", - span { "{language_label} · {source_len} chars" } + span { "rust · " {format!("{} chars", source().chars().count())} } span { class: "editor-meta-divider" } Select:: { value: Some(value.into()), @@ -735,10 +732,9 @@ fn Playground( ClientOnly { CodeEditor { value: source(), - language, + language: Language::Rust, theme, - aria_label: "Source editor", - placeholder: "Type code...", + aria_label: "Rust source editor", class: "playground-code-editor", oninput: move |value| source.set(value), } @@ -785,7 +781,7 @@ fn Docs(scheme: Scheme) -> Element { } div { class: "card-code-body", Code { - src: SourceCode::builder(step.code).with_language(step.language), + src: SourceCode::new(step.language, step.code), theme, } } diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index ccaeca0..12d60c0 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -15,8 +15,7 @@ fn App() -> Element { theme: Theme::RUSTDOC_AYU, } Code { - src: SourceCode::builder("fn main() {\n println!(\"runtime\");\n}") - .with_language(Language::Rust), + src: SourceCode::new(Language::Rust, "fn main() {\n println!(\"runtime\");\n}"), theme: Theme::GITHUB_LIGHT, } } diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 8e8b970..b80fd6a 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -11,10 +11,10 @@ publish = false [dependencies] dioxus = { version = "0.7.0" } -dioxus-code = { workspace = true, features = ["all-languages", "detection"] } +dioxus-code = { workspace = true, features = ["runtime"] } dioxus-code-editor = { workspace = true } [features] default = ["desktop"] -desktop = ["dioxus/desktop", "dioxus-code-editor/desktop"] -web = ["dioxus/web", "dioxus-code-editor/web"] +desktop = ["dioxus/desktop"] +web = ["dioxus/web"] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 44957e2..91765e6 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -33,8 +33,6 @@ fn App() -> Element { let mut read_only = use_signal(|| false); let (theme_label, theme) = THEMES[theme_index()]; - let language = Language::detect(&source()).unwrap_or(Language::Rust); - let language_label = language.slug(); rsx! { style { {APP_CSS} } @@ -84,17 +82,17 @@ fn App() -> Element { } section { class: "editor-frame", div { class: "frame-bar", - span { "{language_label}" } + span { "fizzbuzz.rs" } span { "{theme_label}" } } CodeEditor { value: source(), - language, + language: Language::Rust, theme: CodeTheme::fixed(theme), line_numbers: line_numbers(), read_only: read_only(), - aria_label: "Source editor", - placeholder: "Type code...", + aria_label: "Rust source editor", + placeholder: "Type Rust code...", class: "example-editor", oninput: move |value| source.set(value), } diff --git a/examples/live-input/src/main.rs b/examples/live-input/src/main.rs index d404c9a..66d0fa1 100644 --- a/examples/live-input/src/main.rs +++ b/examples/live-input/src/main.rs @@ -36,7 +36,7 @@ fn App() -> Element { span { "runtime" } } Code { - src: SourceCode::builder(source()).with_language(Language::Rust), + src: SourceCode::new(Language::Rust, source()), theme: Theme::TOKYO_NIGHT, } } diff --git a/src/advanced.rs b/src/advanced.rs index b76767a..afb7157 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -22,9 +22,6 @@ use std::{borrow::Cow, fmt, ops::Range}; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum HighlightError { - /// [`Language::Auto`] could not detect a supported language. - #[cfg(feature = "detection")] - LanguageDetectionFailed, /// The tree-sitter parser rejected the selected grammar. GrammarLoad { /// The language whose grammar failed to load. @@ -95,8 +92,6 @@ impl HighlightError { impl fmt::Display for HighlightError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - #[cfg(feature = "detection")] - Self::LanguageDetectionFailed => write!(f, "could not detect language"), Self::GrammarLoad { language, message } => { write!( f, @@ -629,7 +624,6 @@ impl Buffer { /// ``` pub fn new(language: Language, source: impl ToString) -> Result { let source = source.to_string(); - let language = resolve_language(language, &source)?; let (mut parser, incremental) = Self::parser_for(language)?; let mut cursor = arborium_tree_sitter::QueryCursor::new(); let (tree, spans) = Self::parse_source( @@ -739,7 +733,6 @@ impl Buffer { /// assert_eq!(buffer.language(), Language::Rust); /// ``` pub fn set_language(&mut self, language: Language) -> Result<(), HighlightError> { - let language = resolve_language(language, &self.source)?; if self.language == language { return Ok(()); } @@ -860,23 +853,11 @@ fn collect_spans( normalize_spans(raw) } -#[cfg(feature = "runtime")] -fn resolve_language(language: Language, _source: &str) -> Result { - #[cfg(feature = "detection")] - if language == Language::Auto { - return Language::detect(_source).ok_or(HighlightError::LanguageDetectionFailed); - } - - Ok(language) -} - #[cfg(feature = "runtime")] fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'static str) { // Rust is bundled with the `runtime` feature; everything else is opt-in via // its `lang-*` cargo feature (or the `all-languages` umbrella). match language { - #[cfg(feature = "detection")] - Language::Auto => unreachable!("auto language must be resolved before loading a grammar"), Language::Rust => ( arborium::lang_rust::language(), arborium::lang_rust::HIGHLIGHTS_QUERY, diff --git a/src/language.rs b/src/language.rs index f5c1e64..39663af 100644 --- a/src/language.rs +++ b/src/language.rs @@ -1,9 +1,8 @@ //! Tree-sitter grammar selection. //! -//! [`Language`] is a closed set of language slugs. Grammar variants are gated -//! by the same cargo feature as the corresponding grammar in `dioxus-code`, so -//! the enum only ever exposes variants whose grammar is actually compiled into -//! this build. [`Language::Auto`] is available with the `detection` feature. +//! [`Language`] is a closed set of Arborium language slugs. Each named variant +//! is gated by the same cargo feature as the corresponding grammar in +//! `dioxus-code`, so the enum exposes the variants compiled into this build. macro_rules! define_languages { ( @@ -14,11 +13,10 @@ macro_rules! define_languages { ) => { /// Tree-sitter grammar identifier. /// - /// Each variant maps to a language slug via [`Language::slug`]. - /// Grammar variants are gated by the same `lang-*` cargo features as - /// the grammar lookup table, so unsupported builds simply don't expose - /// those variants. [`Language::Auto`] is available with the - /// `detection` feature. + /// Each variant maps to an Arborium language slug via + /// [`Language::slug`]. Variants are gated by the same `lang-*` cargo + /// features as the grammar lookup table, so each build exposes the + /// variants enabled for that build. /// /// ```rust /// use dioxus_code::Language; @@ -28,12 +26,9 @@ macro_rules! define_languages { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Language { - /// Automatically detect the language from the source text. - #[cfg(feature = "detection")] - Auto, $( $(#[$attr])* - #[doc = concat!("Language slug `\"", $slug, "\"`.")] + #[doc = concat!("Arborium slug `\"", $slug, "\"`.")] $variant, )* } @@ -49,19 +44,15 @@ macro_rules! define_languages { /// assert!(Language::ALL.contains(&Language::Rust)); /// ``` pub const ALL: &'static [Language] = &[ - #[cfg(feature = "detection")] - Self::Auto, $( $(#[$attr])* Self::$variant, )* ]; - /// Stable slug for this language. + /// Arborium slug for this language. pub const fn slug(self) -> &'static str { match self { - #[cfg(feature = "detection")] - Self::Auto => "auto", $( $(#[$attr])* Self::$variant => $slug, @@ -69,14 +60,12 @@ macro_rules! define_languages { } } - /// Parse a language slug into a [`Language`]. + /// Parse an Arborium slug into a [`Language`]. /// /// Returns `None` for unknown slugs and for slugs whose grammar /// feature is disabled in this build. pub fn from_slug(slug: &str) -> Option { match slug { - #[cfg(feature = "detection")] - "auto" => Some(Self::Auto), $( $(#[$attr])* $slug => Some(Self::$variant), @@ -91,24 +80,22 @@ macro_rules! define_languages { impl Language { /// Best-effort detection from a path, filename, shebang, or file contents. /// - /// Uses Arborium's path and shebang detector. With the `detection` feature - /// enabled, this also falls back to betlang source-language inference from - /// source text. The detected slug is mapped into a [`Language`] variant, - /// returning `None` when detection fails or the detected language's grammar - /// feature is disabled in this build. + /// Wraps [`arborium::detect_language`] and maps the resulting slug into a + /// [`Language`] variant, returning `None` when detection fails or the + /// detected language's grammar feature is disabled in this build. /// /// Available with the `runtime` feature. #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] pub fn detect(input: &str) -> Option { - let detected = arborium::detect_language(input).and_then(Self::from_detected_slug); + let detected = arborium::detect_language(input).and_then(Self::from_slug); #[cfg(feature = "detection")] { detected.or_else(|| { betlang::detect(input) .language() - .and_then(|language| Self::from_detected_slug(language.slug())) + .and_then(|language| Self::from_slug(language.slug())) }) } @@ -117,15 +104,6 @@ impl Language { detected } } - - #[cfg(feature = "runtime")] - fn from_detected_slug(slug: &str) -> Option { - match Self::from_slug(slug) { - #[cfg(feature = "detection")] - Some(Self::Auto) => None, - language => language, - } - } } define_languages! { diff --git a/src/lib.rs b/src/lib.rs index cc048dd..158c6c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,9 +11,6 @@ use std::collections::HashMap; mod language; pub use language::Language; -#[cfg(all(feature = "runtime", target_family = "wasm"))] -mod wasm_ctype; - const CODE_CSS: Asset = asset!("/assets/dioxus-code.css"); #[cfg(feature = "macro")] @@ -218,7 +215,7 @@ pub use advanced::{HighlightError, HighlightQueryErrorKind}; /// Source text to highlight at runtime. /// /// Available with the `runtime` feature. Build one with [`SourceCode::new`], -/// or the source-first [`SourceCode::builder`], then pass it to [`Code()`]. +/// then pass it to [`Code()`]. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; @@ -232,19 +229,6 @@ pub struct SourceCode { language: Language, } -/// Source-first builder for [`SourceCode`]. -/// -/// ```rust -/// use dioxus_code::{Language, SourceCode}; -/// let _src = SourceCode::builder("fn main() {}").with_language(Language::Rust); -/// ``` -#[cfg(feature = "runtime")] -#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceCodeBuilder { - source: String, -} - #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] impl SourceCode { @@ -261,18 +245,6 @@ impl SourceCode { } } - /// Start a source-first builder. - /// - /// ```rust - /// use dioxus_code::{Language, SourceCode}; - /// let _src = SourceCode::builder("fn main() {}").with_language(Language::Rust); - /// ``` - pub fn builder(source: impl ToString) -> SourceCodeBuilder { - SourceCodeBuilder { - source: source.to_string(), - } - } - /// Replace the language used to highlight this source. /// /// To set the language from a runtime slug, use [`Language::from_slug`] @@ -305,20 +277,6 @@ impl SourceCode { } } -#[cfg(feature = "runtime")] -#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] -impl SourceCodeBuilder { - /// Finish the builder with an explicit language. - /// - /// ```rust - /// use dioxus_code::{Language, SourceCode}; - /// let _src = SourceCode::builder("fn main() {}").with_language(Language::Rust); - /// ``` - pub fn with_language(self, language: Language) -> SourceCode { - SourceCode::new(language, self.source) - } -} - #[cfg(feature = "runtime")] pub(crate) struct RawHighlightSpan { pub(crate) start: u32, @@ -519,42 +477,6 @@ mod tests { })); } - #[cfg(feature = "runtime")] - #[test] - fn runtime_source_code_builder_highlights() { - let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}") - .with_language(Language::Rust) - .into(); - assert_eq!(tree.language(), Language::Rust); - assert!(tree.spans().iter().any(|span| { - span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" - })); - } - - #[cfg(feature = "runtime")] - #[test] - fn language_detect_remains_available_with_runtime() { - assert_eq!(Language::detect("demo.rs"), Some(Language::Rust)); - } - - #[cfg(feature = "detection")] - #[test] - fn runtime_source_code_auto_language_highlights_detected_source() { - let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}") - .with_language(Language::Auto) - .into(); - assert_eq!(tree.language(), Language::Rust); - assert!(tree.spans().iter().any(|span| { - span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn" - })); - } - - #[cfg(feature = "detection")] - #[test] - fn auto_language_is_available_when_detection_enabled() { - assert!(Language::ALL.contains(&Language::Auto)); - } - #[cfg(feature = "macro")] #[test] fn code_str_macro_highlights_inline_source() { diff --git a/src/wasm_ctype.rs b/src/wasm_ctype.rs deleted file mode 100644 index 14dd20f..0000000 --- a/src/wasm_ctype.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! C ctype symbols needed by Arborium grammar scanners in WASM builds. - -#![allow(missing_docs)] - -use std::ffi::c_int; - -#[inline] -const fn is_lower(c: c_int) -> bool { - c >= b'a' as c_int && c <= b'z' as c_int -} - -#[inline] -const fn is_upper(c: c_int) -> bool { - c >= b'A' as c_int && c <= b'Z' as c_int -} - -#[inline] -const fn is_digit(c: c_int) -> bool { - c >= b'0' as c_int && c <= b'9' as c_int -} - -#[unsafe(no_mangle)] -pub extern "C" fn isalpha(c: c_int) -> c_int { - (is_lower(c) || is_upper(c)) as c_int -} - -#[unsafe(no_mangle)] -pub extern "C" fn isupper(c: c_int) -> c_int { - is_upper(c) as c_int -} - -#[unsafe(no_mangle)] -pub extern "C" fn isxdigit(c: c_int) -> c_int { - (is_digit(c) - || (c >= b'a' as c_int && c <= b'f' as c_int) - || (c >= b'A' as c_int && c <= b'F' as c_int)) as c_int -} - -#[unsafe(no_mangle)] -pub extern "C" fn isspace(c: c_int) -> c_int { - matches!( - c, - x if x == b' ' as c_int - || x == b'\t' as c_int - || x == b'\n' as c_int - || x == b'\r' as c_int - || x == 0x0c - || x == 0x0b - ) as c_int -} - -#[unsafe(no_mangle)] -pub extern "C" fn tolower(c: c_int) -> c_int { - if is_upper(c) { - c + (b'a' - b'A') as c_int - } else { - c - } -} - -#[unsafe(no_mangle)] -pub extern "C" fn iswpunct(wc: u32) -> c_int { - matches!(wc, 0x21..=0x2f | 0x3a..=0x40 | 0x5b..=0x60 | 0x7b..=0x7e) as c_int -} From e4ac5e8232ca1cc95310dbfbc5901b1f18bce493 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 14:32:09 -0500 Subject: [PATCH 04/15] restore language auto --- src/advanced.rs | 19 +++++++++++++++++++ src/language.rs | 7 +++++++ src/lib.rs | 24 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/advanced.rs b/src/advanced.rs index afb7157..b76767a 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -22,6 +22,9 @@ use std::{borrow::Cow, fmt, ops::Range}; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum HighlightError { + /// [`Language::Auto`] could not detect a supported language. + #[cfg(feature = "detection")] + LanguageDetectionFailed, /// The tree-sitter parser rejected the selected grammar. GrammarLoad { /// The language whose grammar failed to load. @@ -92,6 +95,8 @@ impl HighlightError { impl fmt::Display for HighlightError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + #[cfg(feature = "detection")] + Self::LanguageDetectionFailed => write!(f, "could not detect language"), Self::GrammarLoad { language, message } => { write!( f, @@ -624,6 +629,7 @@ impl Buffer { /// ``` pub fn new(language: Language, source: impl ToString) -> Result { let source = source.to_string(); + let language = resolve_language(language, &source)?; let (mut parser, incremental) = Self::parser_for(language)?; let mut cursor = arborium_tree_sitter::QueryCursor::new(); let (tree, spans) = Self::parse_source( @@ -733,6 +739,7 @@ impl Buffer { /// assert_eq!(buffer.language(), Language::Rust); /// ``` pub fn set_language(&mut self, language: Language) -> Result<(), HighlightError> { + let language = resolve_language(language, &self.source)?; if self.language == language { return Ok(()); } @@ -853,11 +860,23 @@ fn collect_spans( normalize_spans(raw) } +#[cfg(feature = "runtime")] +fn resolve_language(language: Language, _source: &str) -> Result { + #[cfg(feature = "detection")] + if language == Language::Auto { + return Language::detect(_source).ok_or(HighlightError::LanguageDetectionFailed); + } + + Ok(language) +} + #[cfg(feature = "runtime")] fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'static str) { // Rust is bundled with the `runtime` feature; everything else is opt-in via // its `lang-*` cargo feature (or the `all-languages` umbrella). match language { + #[cfg(feature = "detection")] + Language::Auto => unreachable!("auto language must be resolved before loading a grammar"), Language::Rust => ( arborium::lang_rust::language(), arborium::lang_rust::HIGHLIGHTS_QUERY, diff --git a/src/language.rs b/src/language.rs index 39663af..774a173 100644 --- a/src/language.rs +++ b/src/language.rs @@ -26,6 +26,9 @@ macro_rules! define_languages { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Language { + /// Automatically detect the language from the source text. + #[cfg(feature = "detection")] + Auto, $( $(#[$attr])* #[doc = concat!("Arborium slug `\"", $slug, "\"`.")] @@ -44,6 +47,8 @@ macro_rules! define_languages { /// assert!(Language::ALL.contains(&Language::Rust)); /// ``` pub const ALL: &'static [Language] = &[ + #[cfg(feature = "detection")] + Self::Auto, $( $(#[$attr])* Self::$variant, @@ -53,6 +58,8 @@ macro_rules! define_languages { /// Arborium slug for this language. pub const fn slug(self) -> &'static str { match self { + #[cfg(feature = "detection")] + Self::Auto => "auto", $( $(#[$attr])* Self::$variant => $slug, diff --git a/src/lib.rs b/src/lib.rs index 158c6c4..ab41743 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,6 +229,14 @@ pub struct SourceCode { language: Language, } +/// Source-first builder for [`SourceCode`]. +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceCodeBuilder { + source: String, +} + #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] impl SourceCode { @@ -245,6 +253,13 @@ impl SourceCode { } } + /// Start a source-first builder. + pub fn builder(source: impl ToString) -> SourceCodeBuilder { + SourceCodeBuilder { + source: source.to_string(), + } + } + /// Replace the language used to highlight this source. /// /// To set the language from a runtime slug, use [`Language::from_slug`] @@ -277,6 +292,15 @@ impl SourceCode { } } +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +impl SourceCodeBuilder { + /// Finish the builder with an explicit language. + pub fn with_language(self, language: Language) -> SourceCode { + SourceCode::new(language, self.source) + } +} + #[cfg(feature = "runtime")] pub(crate) struct RawHighlightSpan { pub(crate) start: u32, From 6a86f9bdf6fe49d4ca4e613f7dd86f5c455901e0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 15:36:13 -0500 Subject: [PATCH 05/15] split out a source detection method --- Cargo.lock | 1 + Cargo.toml | 2 +- dioxus-code-macro/Cargo.toml | 2 + dioxus-code-macro/README.md | 12 +++ dioxus-code-macro/src/lib.rs | 175 +++++++++++++++++++++++++------- src/advanced.rs | 57 ++++++++++- src/language.rs | 191 ++++++++++++++++++++++++++++++++--- src/lib.rs | 12 +++ 8 files changed, 401 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4749f6f..b4f891a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2461,6 +2461,7 @@ version = "0.1.1" dependencies = [ "arborium", "arborium-theme", + "betlang", "dioxus-code", "macro-string", "proc-macro-crate 3.5.0", diff --git a/Cargo.toml b/Cargo.toml index 7fd385c..0b008e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["macro"] macro = ["dep:dioxus-code-macro", "dioxus-code-macro/lang-rust"] runtime = ["arborium/lang-rust", "dep:arborium-tree-sitter"] -detection = ["runtime", "dep:betlang"] +detection = ["runtime", "dep:betlang", "dioxus-code-macro?/detection"] all-languages = [ "runtime", "arborium/all-languages", diff --git a/dioxus-code-macro/Cargo.toml b/dioxus-code-macro/Cargo.toml index a492695..bf2d163 100644 --- a/dioxus-code-macro/Cargo.toml +++ b/dioxus-code-macro/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] default = [] +detection = ["dep:betlang"] all-languages = ["arborium/all-languages"] lang-rust = ["arborium/lang-rust"] lang-ada = ["arborium/lang-ada"] @@ -123,6 +124,7 @@ lang-zsh = ["arborium/lang-zsh"] [dependencies] arborium = { version = "2.16.0", default-features = false } arborium-theme = "2.16.0" +betlang = { version = "0.1.0", optional = true } macro-string = "0.1.4" proc-macro-crate = "3.5.0" proc-macro2 = "1.0.103" diff --git a/dioxus-code-macro/README.md b/dioxus-code-macro/README.md index c3e914b..e0fdc28 100644 --- a/dioxus-code-macro/README.md +++ b/dioxus-code-macro/README.md @@ -50,6 +50,18 @@ let _tree = code!( ); ``` +## Inline source detection + +With the `detection` feature enabled, `code_str!` can infer the language from +inline source contents when no explicit language is provided. The detected +language still needs its matching `lang-*` feature or `all-languages` enabled. + +```rust +use dioxus_code::code_str; + +let _tree = code_str!("fn main() { println!(\"hi\"); }"); +``` + ## License MIT. diff --git a/dioxus-code-macro/src/lib.rs b/dioxus-code-macro/src/lib.rs index 998822f..b3a286b 100644 --- a/dioxus-code-macro/src/lib.rs +++ b/dioxus-code-macro/src/lib.rs @@ -40,9 +40,10 @@ pub fn code(input: TokenStream) -> TokenStream { /// /// Parses a string literal containing source code with [`arborium`] and /// expands to the resulting span tree. Pass the source as a string literal, -/// `concat!(...)`, `include_str!(...)`, or `env!(...)`. The language must be -/// supplied via [`CodeOptions::builder`] with [`CodeOptions::with_language`] -/// since there is no file extension to infer from. +/// `concat!(...)`, `include_str!(...)`, or `env!(...)`. Pass +/// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to name the +/// language explicitly; otherwise, with the macro crate's `detection` feature +/// enabled, the language is inferred from the source contents. /// /// To highlight a file on disk instead, use [`code!`]. /// @@ -106,7 +107,7 @@ fn parse_string_and_options( Ok((value, options)) } -fn try_extract_language(expr: &Expr) -> Option { +fn try_extract_language(expr: &Expr) -> Option { match expr { Expr::Group(group) => try_extract_language(&group.expr), Expr::Paren(paren) => try_extract_language(&paren.expr), @@ -123,7 +124,7 @@ fn try_extract_language(expr: &Expr) -> Option { } } -fn try_parse_language_arg(expr: &Expr) -> Option { +fn try_parse_language_arg(expr: &Expr) -> Option { match expr { Expr::Group(group) => try_parse_language_arg(&group.expr), Expr::Paren(paren) => try_parse_language_arg(&paren.expr), @@ -131,7 +132,7 @@ fn try_parse_language_arg(expr: &Expr) -> Option { try_parse_language_arg(call.args.first().unwrap()) } Expr::Path(path) if is_none_path(path) => None, - Expr::Path(path) => language_slug_from_path(path).map(str::to_string), + Expr::Path(path) => language_spec_from_path(path), _ => None, } } @@ -153,6 +154,12 @@ fn is_none_path(path: &syn::ExprPath) -> bool { .is_some_and(|segment| segment.ident == "None") } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct LanguageSpec { + variant: &'static str, + slug: &'static str, +} + const LANGUAGE_VARIANTS: &[(&str, &str)] = &[ ("Rust", "rust"), ("Ada", "ada"), @@ -259,19 +266,97 @@ const LANGUAGE_VARIANTS: &[(&str, &str)] = &[ ("Zsh", "zsh"), ]; -fn language_slug_from_path(path: &syn::ExprPath) -> Option<&'static str> { +fn language_spec_from_path(path: &syn::ExprPath) -> Option { let variant = path.path.segments.last()?.ident.to_string(); + language_spec_for_variant(&variant) +} + +fn language_spec_for_variant(variant: &str) -> Option { LANGUAGE_VARIANTS .iter() .find(|(name, _)| *name == variant) - .map(|(_, slug)| *slug) + .map(|(variant, slug)| LanguageSpec { + variant: *variant, + slug: *slug, + }) } -fn language_variant_for_slug(slug: &str) -> Option<&'static str> { +fn language_spec_for_slug(slug: &str) -> Option { LANGUAGE_VARIANTS .iter() .find(|(_, s)| *s == slug) - .map(|(name, _)| *name) + .map(|(variant, slug)| LanguageSpec { + variant: *variant, + slug: *slug, + }) +} + +#[cfg(feature = "detection")] +fn detect_source_language(source: &str) -> Option { + betlang::detect(source) + .language() + .and_then(language_spec_for_betlang) +} + +#[cfg(not(feature = "detection"))] +fn detect_source_language(_source: &str) -> Option { + None +} + +#[cfg(feature = "detection")] +fn language_spec_for_betlang(language: betlang::Language) -> Option { + match language { + betlang::Language::Asm => language_spec_for_variant("Asm"), + betlang::Language::Batch => language_spec_for_variant("Batch"), + betlang::Language::C => language_spec_for_variant("C"), + betlang::Language::Clojure => language_spec_for_variant("Clojure"), + betlang::Language::CMake => language_spec_for_variant("CMake"), + betlang::Language::Cobol => language_spec_for_variant("Cobol"), + betlang::Language::Cpp => language_spec_for_variant("Cpp"), + betlang::Language::Cs => language_spec_for_variant("CSharp"), + betlang::Language::Css => language_spec_for_variant("Css"), + betlang::Language::Dart => language_spec_for_variant("Dart"), + betlang::Language::Dockerfile => language_spec_for_variant("Dockerfile"), + betlang::Language::Elixir => language_spec_for_variant("Elixir"), + betlang::Language::Erlang => language_spec_for_variant("Erlang"), + betlang::Language::Gemfile | betlang::Language::Gemspec | betlang::Language::Ruby => { + language_spec_for_variant("Ruby") + } + betlang::Language::Go => language_spec_for_variant("Go"), + betlang::Language::Gradle | betlang::Language::Groovy => { + language_spec_for_variant("Groovy") + } + betlang::Language::Haskell => language_spec_for_variant("Haskell"), + betlang::Language::Html => language_spec_for_variant("Html"), + betlang::Language::Ini => language_spec_for_variant("Ini"), + betlang::Language::Java => language_spec_for_variant("Java"), + betlang::Language::JavaScript => language_spec_for_variant("JavaScript"), + betlang::Language::Json => language_spec_for_variant("Json"), + betlang::Language::Julia => language_spec_for_variant("Julia"), + betlang::Language::Kotlin => language_spec_for_variant("Kotlin"), + betlang::Language::Lisp => language_spec_for_variant("CommonLisp"), + betlang::Language::Lua => language_spec_for_variant("Lua"), + betlang::Language::Markdown => language_spec_for_variant("Markdown"), + betlang::Language::ObjectiveC => language_spec_for_variant("ObjectiveC"), + betlang::Language::Ocaml => language_spec_for_variant("OCaml"), + betlang::Language::Perl => language_spec_for_variant("Perl"), + betlang::Language::Php => language_spec_for_variant("Php"), + betlang::Language::Powershell => language_spec_for_variant("PowerShell"), + betlang::Language::Python => language_spec_for_variant("Python"), + betlang::Language::R => language_spec_for_variant("R"), + betlang::Language::Rust => language_spec_for_variant("Rust"), + betlang::Language::Scala => language_spec_for_variant("Scala"), + betlang::Language::Shell => language_spec_for_variant("Bash"), + betlang::Language::Sql => language_spec_for_variant("Sql"), + betlang::Language::Swift => language_spec_for_variant("Swift"), + betlang::Language::Toml => language_spec_for_variant("Toml"), + betlang::Language::TypeScript => language_spec_for_variant("TypeScript"), + betlang::Language::Vba => language_spec_for_variant("VisualBasic"), + betlang::Language::Verilog => language_spec_for_variant("Verilog"), + betlang::Language::Xml => language_spec_for_variant("Xml"), + betlang::Language::Yaml => language_spec_for_variant("Yaml"), + _ => None, + } } fn expand_code(input: CodeInput) -> syn::Result { @@ -300,18 +385,23 @@ fn expand_shared( let crate_path = dioxus_code_crate_path()?; let options_check = options_check_tokens(&crate_path, options.as_ref()); - let Some(language) = options.as_ref().and_then(try_extract_language).or_else(|| { - origin_path - .as_ref() - .and_then(|path| arborium::detect_language(&path.to_string_lossy()).map(str::to_string)) - }) else { + let Some(language) = options + .as_ref() + .and_then(try_extract_language) + .or_else(|| { + origin_path.as_ref().and_then(|path| { + arborium::detect_language(&path.to_string_lossy()).and_then(language_spec_for_slug) + }) + }) + .or_else(|| detect_source_language(&source)) + else { let message = match origin_path.as_ref() { Some(path) => format!( - "could not detect language for `{}`; pass `CodeOptions::builder().with_language(Language::Rust)`", + "could not detect language for `{}`; pass `CodeOptions::builder().with_language(Language::Rust)` or enable `detection` with the matching `lang-*` feature or `all-languages`", path.display() ), None => String::from( - "could not determine language for `code_str!`; pass `CodeOptions::builder().with_language(Language::Rust)`", + "could not determine language for `code_str!`; pass `CodeOptions::builder().with_language(Language::Rust)` or enable `detection` with the matching `lang-*` feature or `all-languages`", ), }; return Ok(quote! {{ @@ -322,17 +412,10 @@ fn expand_shared( let mut highlighter = arborium::Highlighter::new(); let spans = highlighter - .highlight_spans(&language, &source) + .highlight_spans(language.slug, &source) .map_err(|error| syn::Error::new(Span::call_site(), error.to_string()))?; - let Some(variant) = language_variant_for_slug(&language) else { - let message = format!("language `{language}` has no `Language` variant"); - return Ok(quote! {{ - #options_check - compile_error!(#message); - }}); - }; - let variant_ident = Ident::new(variant, Span::call_site()); + let variant_ident = Ident::new(language.variant, Span::call_site()); let source_expr = match origin_path { Some(path) => { @@ -472,37 +555,59 @@ fn resolve_manifest_path(manifest_dir: &Path, path: &str) -> PathBuf { mod tests { use super::*; - fn language(expr: &str) -> Option { + fn language(expr: &str) -> Option { let expr = syn::parse_str::(expr).unwrap(); try_extract_language(&expr) } + fn slug(expr: &str) -> Option<&'static str> { + language(expr).map(|language| language.slug) + } + #[test] fn extracts_language_variant_options() { assert_eq!( - language("CodeOptions::builder().with_language(Language::Rust)").as_deref(), + slug("CodeOptions::builder().with_language(Language::Rust)"), Some("rust"), ); assert_eq!( - language("CodeOptions::builder().with_language(Some(Language::Rust))").as_deref(), + slug("CodeOptions::builder().with_language(Some(Language::Rust))"), Some("rust"), ); } #[test] fn extracts_none_language_option() { - assert_eq!( - language("CodeOptions::builder().with_language(None)").as_deref(), - None, - ); + assert_eq!(slug("CodeOptions::builder().with_language(None)"), None,); } #[test] fn unknown_method_chains_fall_back_silently() { - assert_eq!(language("CodeOptions::builder()").as_deref(), None); + assert_eq!(slug("CodeOptions::builder()"), None); assert_eq!( - language("CodeOptions::builder().with_themes(Language::Rust)").as_deref(), + slug("CodeOptions::builder().with_themes(Language::Rust)"), None, ); } + + #[cfg(feature = "detection")] + #[test] + fn maps_betlang_languages_directly() { + macro_rules! assert_betlang_mapping { + ($betlang:expr, $variant:literal, $slug:literal) => { + assert_eq!( + language_spec_for_betlang($betlang), + Some(LanguageSpec { + variant: $variant, + slug: $slug, + }) + ); + }; + } + + assert_betlang_mapping!(betlang::Language::Cs, "CSharp", "c-sharp"); + assert_betlang_mapping!(betlang::Language::Lisp, "CommonLisp", "commonlisp"); + assert_betlang_mapping!(betlang::Language::Shell, "Bash", "bash"); + assert_betlang_mapping!(betlang::Language::Vba, "VisualBasic", "vb"); + } } diff --git a/src/advanced.rs b/src/advanced.rs index b76767a..a1dc30c 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -864,7 +864,7 @@ fn collect_spans( fn resolve_language(language: Language, _source: &str) -> Result { #[cfg(feature = "detection")] if language == Language::Auto { - return Language::detect(_source).ok_or(HighlightError::LanguageDetectionFailed); + return Language::detect_source(_source).ok_or(HighlightError::LanguageDetectionFailed); } Ok(language) @@ -1534,6 +1534,61 @@ mod buffer_tests { ); } + #[cfg(all(feature = "detection", feature = "lang-markdown"))] + #[test] + fn auto_detection_uses_source_content_not_path_suffix() { + let source = r#"# Betlang Fixture + +This Markdown file gives the detector a document-shaped source example. + +## Languages + +- Rust +- Python +- JavaScript + +```rust +fn main() { + println!("hello"); +} +``` + +The surrounding prose and headings should make this look like Markdown rather +than the fenced source language. + +main.rs"#; + let buffer = Buffer::new(Language::Auto, source).unwrap(); + + assert_eq!(buffer.language(), Language::Markdown); + } + + #[cfg(all(feature = "detection", feature = "lang-c-sharp"))] + #[test] + fn auto_detection_maps_betlang_csharp_variant() { + let source = r#"using System; +using System.Collections.Generic; +using System.Linq; + +namespace Betlang.Fixtures +{ + public sealed class Program + { + public static void Main(string[] args) + { + var names = new List { "Ada", "Grace", "Linus" }; + foreach (var name in names.Where(value => value.Length > 0)) + { + Console.WriteLine($"Hello, {name}"); + } + } + } +} +"#; + let buffer = Buffer::new(Language::Auto, source).unwrap(); + + assert_eq!(buffer.language(), Language::CSharp); + } + #[test] fn edit_with_explicit_source_edit() { let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap(); diff --git a/src/language.rs b/src/language.rs index 774a173..a30fd25 100644 --- a/src/language.rs +++ b/src/language.rs @@ -85,30 +85,140 @@ macro_rules! define_languages { } impl Language { - /// Best-effort detection from a path, filename, shebang, or file contents. + /// Best-effort detection from a path or filename. /// - /// Wraps [`arborium::detect_language`] and maps the resulting slug into a + /// Wraps [`arborium::detect_language`] and maps the resulting extension into a /// [`Language`] variant, returning `None` when detection fails or the /// detected language's grammar feature is disabled in this build. + /// With the `detection` feature enabled, use `Language::detect_source` to + /// detect from source contents. /// /// Available with the `runtime` feature. #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] pub fn detect(input: &str) -> Option { - let detected = arborium::detect_language(input).and_then(Self::from_slug); + arborium::detect_language(input).and_then(Self::from_slug) + } - #[cfg(feature = "detection")] - { - detected.or_else(|| { - betlang::detect(input) - .language() - .and_then(|language| Self::from_slug(language.slug())) - }) - } + /// Best-effort detection from source contents. + /// + /// Wraps [`betlang::detect`] and maps the detected language enum directly + /// into a [`Language`] variant, returning `None` when detection fails or + /// the detected language's grammar feature is disabled in this build. + /// + /// Available with the `detection` feature. + /// + /// ```rust + /// use dioxus_code::Language; + /// + /// assert_eq!( + /// Language::detect_source("use std::fmt;\nfn main() { println!(\"hi\"); }"), + /// Some(Language::Rust), + /// ); + /// ``` + #[cfg(feature = "detection")] + #[cfg_attr(docsrs, doc(cfg(feature = "detection")))] + pub fn detect_source(source: &str) -> Option { + betlang::detect(source) + .language() + .and_then(Self::from_betlang_language) + } - #[cfg(not(feature = "detection"))] - { - detected + #[cfg(feature = "detection")] + fn from_betlang_language(language: betlang::Language) -> Option { + match language { + #[cfg(feature = "lang-asm")] + betlang::Language::Asm => Some(Self::Asm), + #[cfg(feature = "lang-batch")] + betlang::Language::Batch => Some(Self::Batch), + #[cfg(feature = "lang-c")] + betlang::Language::C => Some(Self::C), + #[cfg(feature = "lang-clojure")] + betlang::Language::Clojure => Some(Self::Clojure), + #[cfg(feature = "lang-cmake")] + betlang::Language::CMake => Some(Self::CMake), + #[cfg(feature = "lang-cobol")] + betlang::Language::Cobol => Some(Self::Cobol), + #[cfg(feature = "lang-cpp")] + betlang::Language::Cpp => Some(Self::Cpp), + #[cfg(feature = "lang-c-sharp")] + betlang::Language::Cs => Some(Self::CSharp), + #[cfg(feature = "lang-css")] + betlang::Language::Css => Some(Self::Css), + #[cfg(feature = "lang-dart")] + betlang::Language::Dart => Some(Self::Dart), + #[cfg(feature = "lang-dockerfile")] + betlang::Language::Dockerfile => Some(Self::Dockerfile), + #[cfg(feature = "lang-elixir")] + betlang::Language::Elixir => Some(Self::Elixir), + #[cfg(feature = "lang-erlang")] + betlang::Language::Erlang => Some(Self::Erlang), + #[cfg(feature = "lang-ruby")] + betlang::Language::Gemfile | betlang::Language::Gemspec | betlang::Language::Ruby => { + Some(Self::Ruby) + } + #[cfg(feature = "lang-go")] + betlang::Language::Go => Some(Self::Go), + #[cfg(feature = "lang-groovy")] + betlang::Language::Gradle | betlang::Language::Groovy => Some(Self::Groovy), + #[cfg(feature = "lang-haskell")] + betlang::Language::Haskell => Some(Self::Haskell), + #[cfg(feature = "lang-html")] + betlang::Language::Html => Some(Self::Html), + #[cfg(feature = "lang-ini")] + betlang::Language::Ini => Some(Self::Ini), + #[cfg(feature = "lang-java")] + betlang::Language::Java => Some(Self::Java), + #[cfg(feature = "lang-javascript")] + betlang::Language::JavaScript => Some(Self::JavaScript), + #[cfg(feature = "lang-json")] + betlang::Language::Json => Some(Self::Json), + #[cfg(feature = "lang-julia")] + betlang::Language::Julia => Some(Self::Julia), + #[cfg(feature = "lang-kotlin")] + betlang::Language::Kotlin => Some(Self::Kotlin), + #[cfg(feature = "lang-commonlisp")] + betlang::Language::Lisp => Some(Self::CommonLisp), + #[cfg(feature = "lang-lua")] + betlang::Language::Lua => Some(Self::Lua), + #[cfg(feature = "lang-markdown")] + betlang::Language::Markdown => Some(Self::Markdown), + #[cfg(feature = "lang-objc")] + betlang::Language::ObjectiveC => Some(Self::ObjectiveC), + #[cfg(feature = "lang-ocaml")] + betlang::Language::Ocaml => Some(Self::OCaml), + #[cfg(feature = "lang-perl")] + betlang::Language::Perl => Some(Self::Perl), + #[cfg(feature = "lang-php")] + betlang::Language::Php => Some(Self::Php), + #[cfg(feature = "lang-powershell")] + betlang::Language::Powershell => Some(Self::PowerShell), + #[cfg(feature = "lang-python")] + betlang::Language::Python => Some(Self::Python), + #[cfg(feature = "lang-r")] + betlang::Language::R => Some(Self::R), + betlang::Language::Rust => Some(Self::Rust), + #[cfg(feature = "lang-scala")] + betlang::Language::Scala => Some(Self::Scala), + #[cfg(feature = "lang-bash")] + betlang::Language::Shell => Some(Self::Bash), + #[cfg(feature = "lang-sql")] + betlang::Language::Sql => Some(Self::Sql), + #[cfg(feature = "lang-swift")] + betlang::Language::Swift => Some(Self::Swift), + #[cfg(feature = "lang-toml")] + betlang::Language::Toml => Some(Self::Toml), + #[cfg(feature = "lang-typescript")] + betlang::Language::TypeScript => Some(Self::TypeScript), + #[cfg(feature = "lang-vb")] + betlang::Language::Vba => Some(Self::VisualBasic), + #[cfg(feature = "lang-verilog")] + betlang::Language::Verilog => Some(Self::Verilog), + #[cfg(feature = "lang-xml")] + betlang::Language::Xml => Some(Self::Xml), + #[cfg(feature = "lang-yaml")] + betlang::Language::Yaml => Some(Self::Yaml), + _ => None, } } } @@ -320,3 +430,56 @@ define_languages! { #[cfg(feature = "lang-zsh")] Zsh => "zsh", } + +#[cfg(all(test, feature = "detection"))] +mod tests { + use super::*; + + macro_rules! assert_betlang_mapping { + ($betlang:expr, $feature:literal, $language:expr) => { + assert_eq!(Language::from_betlang_language($betlang), { + #[cfg(feature = $feature)] + { + Some($language) + } + #[cfg(not(feature = $feature))] + { + None + } + }); + }; + } + + #[test] + fn maps_betlang_languages_directly() { + assert_eq!( + Language::from_betlang_language(betlang::Language::Rust), + Some(Language::Rust), + ); + + assert_betlang_mapping!(betlang::Language::Cs, "lang-c-sharp", Language::CSharp); + assert_betlang_mapping!( + betlang::Language::Lisp, + "lang-commonlisp", + Language::CommonLisp + ); + assert_betlang_mapping!( + betlang::Language::ObjectiveC, + "lang-objc", + Language::ObjectiveC + ); + assert_betlang_mapping!(betlang::Language::Shell, "lang-bash", Language::Bash); + assert_betlang_mapping!(betlang::Language::Vba, "lang-vb", Language::VisualBasic); + assert_betlang_mapping!(betlang::Language::Gemfile, "lang-ruby", Language::Ruby); + assert_betlang_mapping!(betlang::Language::Gemspec, "lang-ruby", Language::Ruby); + assert_betlang_mapping!(betlang::Language::Gradle, "lang-groovy", Language::Groovy); + } + + #[test] + fn detect_does_not_fall_back_to_source_contents() { + let source = "use std::fmt;\nfn main() { println!(\"hi\"); }"; + + assert_eq!(Language::detect(source), None); + assert_eq!(Language::detect_source(source), Some(Language::Rust)); + } +} diff --git a/src/lib.rs b/src/lib.rs index ab41743..3a80366 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -514,4 +514,16 @@ mod tests { span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn" })); } + + #[cfg(all(feature = "macro", feature = "detection"))] + #[test] + fn code_str_macro_detects_inline_source() { + const TREE: advanced::HighlightedSource = code_str!("fn main() { println!(\"hi\"); }"); + + assert_eq!(TREE.language(), Language::Rust); + assert_eq!(TREE.source(), "fn main() { println!(\"hi\"); }"); + assert!(TREE.spans().iter().any(|span| { + span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn" + })); + } } From 71f5d73177c2bf1a0835ffaa51108848739b7f3f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 15:38:11 -0500 Subject: [PATCH 06/15] use language detection in the demo --- docsite/Cargo.toml | 2 +- docsite/src/main.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index c63503c..0cac5d8 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] dioxus = { version = "0.7.0", features = ["router"] } -dioxus-code = { workspace = true, features = ["runtime", "lang-toml"] } +dioxus-code = { workspace = true, features = ["runtime", "detection", "lang-toml"] } dioxus-code-editor = { workspace = true } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } diff --git a/docsite/src/main.rs b/docsite/src/main.rs index 6a08fd0..84dc8d7 100644 --- a/docsite/src/main.rs +++ b/docsite/src/main.rs @@ -701,9 +701,9 @@ fn Playground( div { class: "playground-grid", Card { class: "card-editor", div { class: "card-bar", - span { "source.rs" } + span { "source" } span { class: "editor-meta", - span { "rust · " {format!("{} chars", source().chars().count())} } + span { "auto · " {format!("{} chars", source().chars().count())} } span { class: "editor-meta-divider" } Select:: { value: Some(value.into()), @@ -732,9 +732,9 @@ fn Playground( ClientOnly { CodeEditor { value: source(), - language: Language::Rust, + language: Language::Auto, theme, - aria_label: "Rust source editor", + aria_label: "Auto-detected source editor", class: "playground-code-editor", oninput: move |value| source.set(value), } From 58b70f5aa951a8491db7cedade8444a92de58dd4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 16:44:26 -0500 Subject: [PATCH 07/15] update the detected language as you type --- code-editor/Cargo.toml | 1 + code-editor/src/lib.rs | 45 ++++++++++++++++++++++++++++++++++++++++-- docsite/Cargo.toml | 2 +- docsite/src/main.rs | 38 ++++++++++++++++++++++++++++++----- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 2a3505d..6493e7a 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -31,3 +31,4 @@ web-sys = { version = "0.3", features = [ [features] all-languages = ["dioxus-code/all-languages"] +detection = ["dioxus-code/detection"] diff --git a/code-editor/src/lib.rs b/code-editor/src/lib.rs index 7e0e7a2..b6af587 100644 --- a/code-editor/src/lib.rs +++ b/code-editor/src/lib.rs @@ -40,7 +40,9 @@ pub struct CodeEditorProps { /// Tree-sitter grammar used for syntax highlighting. /// /// Pass a [`Language`] variant directly. Use [`Language::from_slug`] to - /// turn a runtime slug into a variant. Defaults to [`Language::Rust`]. + /// turn a runtime slug into a variant. With the `detection` feature, pass + /// `Language::Auto` to detect from source contents. Defaults to + /// [`Language::Rust`]. #[props(default = Language::Rust)] pub language: Language, /// Syntax theme selection shared with [`dioxus-code`]. @@ -120,7 +122,9 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { let edit = edit_tracker.borrow_mut().take_for_render(&props.value); let snapshot = { let mut slot = state.borrow_mut(); - if slot.language != props.language { + if slot.language != props.language + || should_rebuild_buffer(props.language, &slot, &props.value) + { slot.buffer = Buffer::new(props.language, props.value.clone()).ok(); slot.language = props.language; } @@ -202,6 +206,23 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { } } +#[cfg(feature = "detection")] +fn should_rebuild_buffer(language: Language, slot: &EditorBuffer, value: &str) -> bool { + if language != Language::Auto { + return false; + } + + match slot.buffer.as_ref() { + Some(buffer) => buffer.source() != value, + None => true, + } +} + +#[cfg(not(feature = "detection"))] +fn should_rebuild_buffer(_language: Language, _slot: &EditorBuffer, _value: &str) -> bool { + false +} + fn editor_class(theme: impl Into, line_numbers: bool, extra_class: &str) -> String { let mut class = format!("dxc-editor {}", theme.into().classes()); if !line_numbers { @@ -254,4 +275,24 @@ mod tests { assert_eq!(lines[0], vec![HighlightSegment::new("let x = 1;", None)]); assert!(lines[1].is_empty()); } + + #[cfg(feature = "detection")] + #[test] + fn auto_language_rebuilds_when_source_changes() { + let source = "use std::fmt;\nfn main() { println!(\"hi\"); }"; + let slot = EditorBuffer { + buffer: Buffer::new(Language::Auto, source).ok(), + language: Language::Auto, + }; + + assert!(!should_rebuild_buffer(Language::Rust, &slot, "print('hi')")); + assert!(!should_rebuild_buffer(Language::Auto, &slot, source)); + assert!(should_rebuild_buffer(Language::Auto, &slot, "print('hi')")); + + let empty_slot = EditorBuffer { + buffer: None, + language: Language::Auto, + }; + assert!(should_rebuild_buffer(Language::Auto, &empty_slot, source)); + } } diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index 0cac5d8..23ff091 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] dioxus = { version = "0.7.0", features = ["router"] } dioxus-code = { workspace = true, features = ["runtime", "detection", "lang-toml"] } -dioxus-code-editor = { workspace = true } +dioxus-code-editor = { workspace = true, features = ["detection"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } [features] diff --git a/docsite/src/main.rs b/docsite/src/main.rs index 84dc8d7..bc9bb0a 100644 --- a/docsite/src/main.rs +++ b/docsite/src/main.rs @@ -544,7 +544,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { "." } p { class: "hero-lede", - "A drop-in component with two source modes: compile-time macro and runtime highlighting with explicit language selection." + "A drop-in component with compile-time macros, runtime highlighting, and optional source-language detection." } div { class: "hero-terminal-block", div { class: "hero-terminal-bar", @@ -663,7 +663,7 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-aside-num", "02" } div { h3 { class: "receipt-aside-title", "SourceCode" } - p { class: "receipt-aside-text", "Pull it in when input is dynamic and pass the language your source uses." } + p { class: "receipt-aside-text", "Pull it in when input is dynamic; pass a language or let detection pick one from source." } } } div { class: "receipt-aside-row", @@ -690,6 +690,8 @@ fn Playground( let theme_pair = theme_pairs[active_idx()]; let theme = theme_pair.code_theme(scheme); let value = use_memo(move || Some(active_idx())); + let detected_language = use_memo(move || detected_language(&source())); + let language_label = use_memo(move || language_label(detected_language())); rsx! { section { id: "playground", class: "section", @@ -703,7 +705,7 @@ fn Playground( div { class: "card-bar", span { "source" } span { class: "editor-meta", - span { "auto · " {format!("{} chars", source().chars().count())} } + span { "{language_label()} · " {format!("{} chars", source().chars().count())} } span { class: "editor-meta-divider" } Select:: { value: Some(value.into()), @@ -732,7 +734,7 @@ fn Playground( ClientOnly { CodeEditor { value: source(), - language: Language::Auto, + language: detected_language().unwrap_or(Language::Rust), theme, aria_label: "Auto-detected source editor", class: "playground-code-editor", @@ -745,6 +747,14 @@ fn Playground( } } +fn detected_language(source: &str) -> Option { + Language::detect_source(source) +} + +fn language_label(language: Option) -> &'static str { + language.map(Language::slug).unwrap_or("unknown") +} + #[component] fn Docs(scheme: Scheme) -> Element { let theme_pair = ThemePair::new( @@ -820,7 +830,7 @@ fn doc_step_data() -> [DocStepData; 3] { num: "02", eyebrow: "Runtime source", title: "SourceCode for live input", - copy: "Pass any string through SourceCode. Provide a language hint when you already know it — Arborium handles tokenizing.", + copy: "Pass any string through SourceCode. Provide a language hint when you know it, or enable detection for source-first workflows.", code: DOCS_RUNTIME, language: Language::Rust, file_name: "runtime.rs", @@ -900,3 +910,21 @@ fn demo_theme_pairs() -> &'static [ThemePair] { } const APP_CSS: Asset = asset!("/assets/app.css"); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn playground_language_label_shows_detected_language() { + let detected = detected_language("use std::fmt;\nfn main() { println!(\"hi\"); }"); + + assert_eq!(detected, Some(Language::Rust)); + assert_eq!(language_label(detected), "rust"); + } + + #[test] + fn playground_language_label_handles_unknown_language() { + assert_eq!(language_label(detected_language("")), "unknown",); + } +} From 91b8ef2a84a000752c157b23e8c6fd4faa3331fa Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 16:56:30 -0500 Subject: [PATCH 08/15] enable all languages for the demo --- docsite/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index 23ff091..8f4f264 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -11,8 +11,8 @@ publish = false [dependencies] dioxus = { version = "0.7.0", features = ["router"] } -dioxus-code = { workspace = true, features = ["runtime", "detection", "lang-toml"] } -dioxus-code-editor = { workspace = true, features = ["detection"] } +dioxus-code = { workspace = true, features = ["detection", "all-languages"] } +dioxus-code-editor = { workspace = true, features = ["detection", "all-languages"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } [features] From 83359c7ccaa410f0f45b2fd40bb1e951d49b3791 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 17:58:53 -0500 Subject: [PATCH 09/15] bump dx in ci --- .github/workflows/nightly.yml | 2 +- .github/workflows/web.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ef621d3..2ff10d1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,5 +45,5 @@ jobs: features: fullstack debug-symbols: false base-path: ${{ github.event.repository.name }} - dx-cli-version: 0.7.7 + dx-cli-version: 0.7.9 toolchain: nightly diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index db90197..640ba34 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -25,4 +25,4 @@ jobs: features: fullstack debug-symbols: false base-path: ${{ github.event.repository.name }} - dx-cli-version: 0.7.7 + dx-cli-version: 0.7.9 From cee8a1edc70a4b75a6237102e94a7016790fa371 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 18:07:16 -0500 Subject: [PATCH 10/15] fix clippy --- dioxus-code-macro/src/lib.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/dioxus-code-macro/src/lib.rs b/dioxus-code-macro/src/lib.rs index b3a286b..b6909a0 100644 --- a/dioxus-code-macro/src/lib.rs +++ b/dioxus-code-macro/src/lib.rs @@ -275,20 +275,14 @@ fn language_spec_for_variant(variant: &str) -> Option { LANGUAGE_VARIANTS .iter() .find(|(name, _)| *name == variant) - .map(|(variant, slug)| LanguageSpec { - variant: *variant, - slug: *slug, - }) + .map(|(variant, slug)| LanguageSpec { variant, slug }) } fn language_spec_for_slug(slug: &str) -> Option { LANGUAGE_VARIANTS .iter() .find(|(_, s)| *s == slug) - .map(|(variant, slug)| LanguageSpec { - variant: *variant, - slug: *slug, - }) + .map(|(variant, slug)| LanguageSpec { variant, slug }) } #[cfg(feature = "detection")] From 076640325c9ac3ab156dfed6af0cff45afcccc57 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 18 May 2026 21:08:54 -0500 Subject: [PATCH 11/15] bump dioxus --- Cargo.lock | 162 ++++++++++++++++----------------- Cargo.toml | 4 +- code-editor/Cargo.toml | 2 +- docsite/Cargo.toml | 2 +- examples/basic/Cargo.toml | 2 +- examples/editor/Cargo.toml | 2 +- examples/live-input/Cargo.toml | 2 +- examples/macro-only/Cargo.toml | 2 +- 8 files changed, 89 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4f891a..b50ae77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2323,9 +2323,9 @@ dependencies = [ [[package]] name = "dioxus" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daed3da3e7678d7267bc8523c607dbd19c970f4154cef30b9a58acf35e0a44d5" +checksum = "7c01ecf7ddbae18a419ad3d83c486101a85ffc5740ea09cdd0f09a30dc12170d" dependencies = [ "dioxus-asset-resolver", "dioxus-cli-config", @@ -2357,9 +2357,9 @@ dependencies = [ [[package]] name = "dioxus-asset-resolver" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28cf8859bac5946200df9dc0de8bdc3df60bec007e465dbd3684dbd05fc3ac7" +checksum = "69387edbbc60c7cb93ad96d8cc7a22b49a76e21643380b89b1c49a78d347ff60" dependencies = [ "dioxus-cli-config", "http", @@ -2389,9 +2389,9 @@ dependencies = [ [[package]] name = "dioxus-cli-config" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef4c22f0a239158f77966d7e7d39b637a10cb374cbd85f881704a336fd3f5c8" +checksum = "c000f584ddf608e2b272b3074bf11512a474eeeb2eb85a1915f276ce5c4a8615" dependencies = [ "wasm-bindgen", ] @@ -2480,9 +2480,9 @@ dependencies = [ [[package]] name = "dioxus-config-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a57abdf7bf60e22732a76588e75274ca4eb5d6399b716c735d187d743060b9" +checksum = "7637091592978fbfdb45a16b26bd99fd97fb1bd7e31c6a963530e00c022af321" dependencies = [ "proc-macro2", "quote", @@ -2490,15 +2490,15 @@ dependencies = [ [[package]] name = "dioxus-config-macros" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b571d361abed46996a489e88ced2a335f0ca608305e238859a1a6cca1d85fb15" +checksum = "54f9ed8fc1a215ad34bb8dbae42a4ea54efbcd26ca9006bbe5cca78e511bf25f" [[package]] name = "dioxus-core" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1ff62d7073a4dd48670093469e2c099ef3c895345998a064554274eb39dc91" +checksum = "45887100ff0cf89abeb8b659808294fda48cd53f3b424e36407dedffcfea830b" dependencies = [ "anyhow", "const_format", @@ -2518,9 +2518,9 @@ dependencies = [ [[package]] name = "dioxus-core-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e22483ccaaf037e600683f25a6114754b9610e85e6fafb11adf45ccc2bd7116" +checksum = "370c63663dff0f24df5dfea643ca239283542c6b228a302f69b32e1d36762b7f" dependencies = [ "convert_case 0.8.0", "dioxus-rsx", @@ -2531,15 +2531,15 @@ dependencies = [ [[package]] name = "dioxus-core-types" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a47a4908cc9680a27b314aa8c3832a517274f896a31b0cce7d7adee57eacbb" +checksum = "36963eab106b169737762f9cd5ee5fd97f585989dcb2d8e30a596e97a6999009" [[package]] name = "dioxus-desktop" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3fc8ad2cb85e7810e35f69eccd31ad1cc715f2718f33b3e6b84ff5c794c6236" +checksum = "662cd78c73ca3f17346adbf2d64757df40dd0ce20536c05123097fd31828d2bd" dependencies = [ "anyhow", "async-trait", @@ -2565,7 +2565,7 @@ dependencies = [ "image", "infer", "jni 0.21.1", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "libc", "muda", "ndk", @@ -2594,9 +2594,9 @@ dependencies = [ [[package]] name = "dioxus-devtools" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58abe580e75d1e6bbab658681b126d4a79df3e9c9fd66d0a0627206d1669f65d" +checksum = "2349cedbdf1b429df1f1bea61fdee0ad3dae7b2548eedfbeca82710122a57da0" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2614,9 +2614,9 @@ dependencies = [ [[package]] name = "dioxus-devtools-types" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5678f9f53962936765d0d767c13200bc6911a943492b8614d666425da62662d2" +checksum = "0ab9b0f7565d1916b70915f59b89ea8054ef0a9d67a364a32bbee68ef5f3818d" dependencies = [ "dioxus-core", "serde", @@ -2625,9 +2625,9 @@ dependencies = [ [[package]] name = "dioxus-document" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28eea4bea227f6eb0852ab67ed97a20474f234e8bb2a6abab17401d443aa746c" +checksum = "37e3a5bec7ffc999ff23446a487eb5cd86111d1574a23533dd3f8b3c69a53a22" dependencies = [ "dioxus-core", "dioxus-core-macro", @@ -2636,7 +2636,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "serde", "serde_json", "tracing", @@ -2644,9 +2644,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75d9db5bb54c6305ed3f1cd71ea245a9935126f7586faf92950b40be76be5ae" +checksum = "37f0558edb88af5ad47275ae36a7f06317163ba482db377c26d7d8590b5cd0f6" dependencies = [ "anyhow", "async-stream", @@ -2709,9 +2709,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack-core" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a1d1f1ff784a78709f95b5021888bcd6bb2a4acf972213acd23c1a8a69701b1" +checksum = "cc634b28b4b1e3eab1e8df4f98510e2d2fa39d686321467f977213155e86ed2b" dependencies = [ "anyhow", "axum-core", @@ -2737,9 +2737,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927db4a9d939677a921eccf2ed36762f05b2e624787f2ef3c095fabc6e6c1c4" +checksum = "85a8fe7da549859fae00c7f4bf11a2aab734ae7ef6f98f280dce9bea1f3326ec" dependencies = [ "const_format", "convert_case 0.8.0", @@ -2751,9 +2751,9 @@ dependencies = [ [[package]] name = "dioxus-history" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e304644c2131e00c4a58ecd91e8f7f86dba007532732bd1a391506b75a974a9" +checksum = "1a15232302d1933015fcf2d6fe9e286ad36f6e9c205a546089a0f326023bb0d2" dependencies = [ "dioxus-core", "tracing", @@ -2761,9 +2761,9 @@ dependencies = [ [[package]] name = "dioxus-hooks" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50506e568a9786247993a29dd3b06fd84f30605312beea2b5f4e51f4ab543b0c" +checksum = "4534f91cf6305204b948bdec130076ac9ecc7c22faab29475b76870558bf73ea" dependencies = [ "dioxus-core", "dioxus-signals", @@ -2777,9 +2777,9 @@ dependencies = [ [[package]] name = "dioxus-html" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c52bdcc2355437ca8bd9986aa1c43c5439af459a2c16012d949b6a35092ae2" +checksum = "e03d6ad4040b667f2b2eefcb678840e630938c09bf9ec39b04ea4d1d96d90d44" dependencies = [ "async-trait", "bytes", @@ -2794,7 +2794,7 @@ dependencies = [ "futures-util", "generational-box", "keyboard-types", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "rustversion", "serde", "serde_json", @@ -2804,9 +2804,9 @@ dependencies = [ [[package]] name = "dioxus-html-internal-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a48657a6a20a65894abba7e7876937e37e924ecaed63e8b654b364c4ffa133" +checksum = "584e2772127ab00f0d5e1d4d9795f39fecebc828ece0b7a02349d438bc1b1ce7" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -2816,15 +2816,15 @@ dependencies = [ [[package]] name = "dioxus-interpreter-js" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a36364418afcb181e19c67d9dbea766dbc2e6bfc0751365a60727ebe665362" +checksum = "11999d6eb5bb179a9512dad30e5de408aab66f2cb65de9098c9fbe02927e2978" dependencies = [ "dioxus-core", "dioxus-core-types", "dioxus-html", "js-sys", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "rustc-hash 2.1.2", "serde", "sledgehammer_bindgen", @@ -2836,9 +2836,9 @@ dependencies = [ [[package]] name = "dioxus-liveview" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea567ceb197345d2719f0808fc8abf508cb47455bf3ee91c805c49d81c2265d" +checksum = "7344b8f174967c7d2f6ad0103d680ab57daea83ebe3368f7f011c402fd6aaf77" dependencies = [ "axum", "dioxus-cli-config", @@ -2864,9 +2864,9 @@ dependencies = [ [[package]] name = "dioxus-logger" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32720fddff9b73266502b15b2c36b139bd8fbfd5b85b67d29ba06d5b4ed60669" +checksum = "a28ccdfe36d2cb830a2784e40f7e6f7199805a2c6da99bd65b1ca308f11aed28" dependencies = [ "dioxus-cli-config", "tracing", @@ -2892,9 +2892,9 @@ dependencies = [ [[package]] name = "dioxus-router" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74cbe73ab4e815aacaa41726d25b84a2debacade3f35db357495dd1d5ed14949" +checksum = "38e47f62d680429badfcb99bf5dec17ee92b0cb9623f264e36bc003a1359bfdc" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2913,9 +2913,9 @@ dependencies = [ [[package]] name = "dioxus-router-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b91aa613f584c2dd598436c33d22b94b85b132b449eae8f81e538300980222" +checksum = "6f83fb667d27e256f8c9eca49963fbace66a8722cb64ee15a10ffc97d092357e" dependencies = [ "base16", "digest", @@ -2928,9 +2928,9 @@ dependencies = [ [[package]] name = "dioxus-rsx" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767e37207d120b978643f5d1f68675984dfa68d44ceb4dab3d4337b36695d339" +checksum = "2106afda239a4c7c22ffa1ca19117011225fc1c735c139c0a5b765996aa8bb1d" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -2953,9 +2953,9 @@ dependencies = [ [[package]] name = "dioxus-server" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba9bd5435d3b92a77f1f6ccb175b009efbb708c02247a786bd5bff4bf1253f3" +checksum = "b5ba2095c16f847d3f680a94cc9b0637d190aace651ecfad0feda180da13634b" dependencies = [ "anyhow", "async-trait", @@ -3011,9 +3011,9 @@ dependencies = [ [[package]] name = "dioxus-signals" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef8e274214375597ad26a2e4407b681de4f876785724171ab605a51b272c51e" +checksum = "3705754f5e043deec9fc7af0d159f18e5b21c02c47d255c7e477f31368f0b6d2" dependencies = [ "dioxus-core", "futures-channel", @@ -3027,9 +3027,9 @@ dependencies = [ [[package]] name = "dioxus-ssr" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa8abccb3124dd16ff0e557c5659fbe00f8ae3b6a29c1be01e4e440fb69f7c7" +checksum = "d261c5c9907b84fb1ed52f59f46d68c84a4ae860a65cc5effd0cea740ee428af" dependencies = [ "askama_escape", "dioxus-core", @@ -3039,9 +3039,9 @@ dependencies = [ [[package]] name = "dioxus-stores" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a6a8e92a6df3b3e875f268a2f20d2aeaf017fc952af86810b4d4e435b9a8c1" +checksum = "64bec7b21c86b1360ec965a07a53a2c96b7caee3465049e1c299a45024e87614" dependencies = [ "dioxus-core", "dioxus-signals", @@ -3051,9 +3051,9 @@ dependencies = [ [[package]] name = "dioxus-stores-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb44211a301fd4ddcb21b9c46807658690c2d5a52a7313393001090764ca1ab" +checksum = "40a5875e9f890f27b1cc3e5b56c1e23601211470315a1fb8627c4ca4f3b2be9a" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -3063,9 +3063,9 @@ dependencies = [ [[package]] name = "dioxus-web" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e3fcfea97e9a1755d06a38c05e9d8b19fd09228ede36f2c9cfab69891c628" +checksum = "bc0a0be76b404e8242a597db0fb239d05f8dee4e7856bc1fc7144f7e244822fd" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -3082,7 +3082,7 @@ dependencies = [ "generational-box", "gloo-timers", "js-sys", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "rustc-hash 2.1.2", "send_wrapper", "serde", @@ -3628,9 +3628,9 @@ dependencies = [ [[package]] name = "generational-box" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155c81d7c7cae205289a26248ede524fdb1b7266b1fdb0a479b57bd3a6db2235" +checksum = "8cd0d825b8d339701ad330dbcd6399519ced4d143484954daf6e3185dace4f77" dependencies = [ "parking_lot", "tracing", @@ -4553,9 +4553,9 @@ checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" [[package]] name = "lazy-js-bundle" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76317b9348f1c069d7e652722c3fa86683ec047ba4c1551bde1daa576ce3807e" +checksum = "ccafada6c9541db44db758619236f2748f6e1bdaa84d04ded858567cd1e89321" [[package]] name = "lazy_static" @@ -4754,9 +4754,9 @@ dependencies = [ [[package]] name = "manganis" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652dba76564ebccd550649b3db9ed608bb5cabfccbe90113c56783649f324eab" +checksum = "8bfcf56309de35b48b8780ea097ace5c3b773a617b52edc49dfc9a63a7d9dc43" dependencies = [ "const-serialize 0.7.2", "const-serialize 0.8.0-alpha.0", @@ -4770,9 +4770,9 @@ dependencies = [ [[package]] name = "manganis-core" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d13cbb4dc790c31565bf49a418e6f441e66130d66aefe9239636e02ab3ebd4" +checksum = "a24d6be68f594495aea60850a284029d585d7b7839b26096c1b6d758f8518648" dependencies = [ "const-serialize 0.7.2", "const-serialize 0.8.0-alpha.0", @@ -4784,9 +4784,9 @@ dependencies = [ [[package]] name = "manganis-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b234972a38639be3b753809ba1d8060ef08b548d07d07cc0c3030aab0c30c132" +checksum = "5e782a10318d707c0833e31876ded8acf91287eee0010af8392559af614c7226" dependencies = [ "dunce", "macro-string", @@ -6690,9 +6690,9 @@ dependencies = [ [[package]] name = "subsecond" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a046b8921cd8a8b3bbeba6339d4ea8cc687653b30c73a158ed4d730d402199db" +checksum = "9cc79674bd55726e6b123204403389400229a95fe4a3b2c5453dada70b06ca95" dependencies = [ "js-sys", "libc", @@ -6709,9 +6709,9 @@ dependencies = [ [[package]] name = "subsecond-types" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3c20eb7361e08d071ee249a687cddf16a1ae9d36f9bcd92481f31d6ac17eee" +checksum = "e9798bfed58797aed51c672aa99810aac30a50d3120ecfdcf28c13784e9a8f1c" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 0b008e9..302bec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -259,7 +259,7 @@ lang-zsh = ["runtime", "arborium/lang-zsh", "dioxus-code-macro?/lang-zsh"] arborium = { version = "2.16.0", default-features = false } arborium-theme = "2.16.0" arborium-tree-sitter = { version = "2.16.0", optional = true } -dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } +dioxus = { version = "0.7.9", default-features = false, features = ["lib"] } dioxus-code-macro = { version = "0.1.0", path = "dioxus-code-macro", default-features = false, optional = true } betlang = { version = "0.1.0", optional = true } @@ -267,6 +267,6 @@ betlang = { version = "0.1.0", optional = true } arborium = { version = "2.16.0", default-features = false } [dev-dependencies] -dioxus = { version = "0.7.0", features = ["ssr"] } +dioxus = { version = "0.7.9", features = ["ssr"] } dioxus-code = { path = ".", features = ["runtime"] } dioxus-code-editor = { workspace = true } diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 6493e7a..6fc4bc0 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -16,7 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"] targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] -dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } +dioxus = { version = "0.7.9", default-features = false, features = ["lib"] } dioxus-code = { workspace = true, features = ["runtime"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index 8f4f264..06c0dcb 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0", features = ["router"] } +dioxus = { version = "0.7.9", features = ["router"] } dioxus-code = { workspace = true, features = ["detection", "all-languages"] } dioxus-code-editor = { workspace = true, features = ["detection", "all-languages"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 6891456..39781e7 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true, features = ["runtime"] } [features] diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index b80fd6a..e44e6a1 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true, features = ["runtime"] } dioxus-code-editor = { workspace = true } diff --git a/examples/live-input/Cargo.toml b/examples/live-input/Cargo.toml index de5dd36..f04eca0 100644 --- a/examples/live-input/Cargo.toml +++ b/examples/live-input/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true, features = ["runtime"] } [features] diff --git a/examples/macro-only/Cargo.toml b/examples/macro-only/Cargo.toml index 5ab787d..31b9fe2 100644 --- a/examples/macro-only/Cargo.toml +++ b/examples/macro-only/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true } [features] From dfc995ac0f3e4f9506e9a511e2f1794b9ca58e47 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 May 2026 08:04:47 -0500 Subject: [PATCH 12/15] only include detectable languages --- Cargo.toml | 46 ++++++++++++++++++++++++++++++++++++++++++ code-editor/Cargo.toml | 1 + docsite/Cargo.toml | 4 ++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 302bec6..64e5c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,52 @@ default = ["macro"] macro = ["dep:dioxus-code-macro", "dioxus-code-macro/lang-rust"] runtime = ["arborium/lang-rust", "dep:arborium-tree-sitter"] detection = ["runtime", "dep:betlang", "dioxus-code-macro?/detection"] +detectable-languages = [ + "lang-asm", + "lang-bash", + "lang-batch", + "lang-c", + "lang-c-sharp", + "lang-clojure", + "lang-cmake", + "lang-cobol", + "lang-commonlisp", + "lang-cpp", + "lang-css", + "lang-dart", + "lang-dockerfile", + "lang-elixir", + "lang-erlang", + "lang-go", + "lang-groovy", + "lang-haskell", + "lang-html", + "lang-ini", + "lang-java", + "lang-javascript", + "lang-json", + "lang-julia", + "lang-kotlin", + "lang-lua", + "lang-markdown", + "lang-objc", + "lang-ocaml", + "lang-perl", + "lang-php", + "lang-powershell", + "lang-python", + "lang-r", + "lang-ruby", + "lang-scala", + "lang-sql", + "lang-swift", + "lang-toml", + "lang-typescript", + "lang-vb", + "lang-verilog", + "lang-xml", + "lang-yaml", +] all-languages = [ "runtime", "arborium/all-languages", diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 6fc4bc0..96535cc 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -31,4 +31,5 @@ web-sys = { version = "0.3", features = [ [features] all-languages = ["dioxus-code/all-languages"] +detectable-languages = ["dioxus-code/detectable-languages"] detection = ["dioxus-code/detection"] diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index 06c0dcb..d22a0c7 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -11,8 +11,8 @@ publish = false [dependencies] dioxus = { version = "0.7.9", features = ["router"] } -dioxus-code = { workspace = true, features = ["detection", "all-languages"] } -dioxus-code-editor = { workspace = true, features = ["detection", "all-languages"] } +dioxus-code = { workspace = true, features = ["detection", "detectable-languages"] } +dioxus-code-editor = { workspace = true, features = ["detection", "detectable-languages"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } [features] From 52f145662f0e7eaf19421e2aec5379898c0b12bf Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 May 2026 09:05:00 -0500 Subject: [PATCH 13/15] simplify language_spec_for_betlang --- dioxus-code-macro/src/lib.rs | 101 +++++++++++++++++------------------ 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/dioxus-code-macro/src/lib.rs b/dioxus-code-macro/src/lib.rs index b6909a0..46b8d64 100644 --- a/dioxus-code-macro/src/lib.rs +++ b/dioxus-code-macro/src/lib.rs @@ -299,58 +299,55 @@ fn detect_source_language(_source: &str) -> Option { #[cfg(feature = "detection")] fn language_spec_for_betlang(language: betlang::Language) -> Option { - match language { - betlang::Language::Asm => language_spec_for_variant("Asm"), - betlang::Language::Batch => language_spec_for_variant("Batch"), - betlang::Language::C => language_spec_for_variant("C"), - betlang::Language::Clojure => language_spec_for_variant("Clojure"), - betlang::Language::CMake => language_spec_for_variant("CMake"), - betlang::Language::Cobol => language_spec_for_variant("Cobol"), - betlang::Language::Cpp => language_spec_for_variant("Cpp"), - betlang::Language::Cs => language_spec_for_variant("CSharp"), - betlang::Language::Css => language_spec_for_variant("Css"), - betlang::Language::Dart => language_spec_for_variant("Dart"), - betlang::Language::Dockerfile => language_spec_for_variant("Dockerfile"), - betlang::Language::Elixir => language_spec_for_variant("Elixir"), - betlang::Language::Erlang => language_spec_for_variant("Erlang"), - betlang::Language::Gemfile | betlang::Language::Gemspec | betlang::Language::Ruby => { - language_spec_for_variant("Ruby") - } - betlang::Language::Go => language_spec_for_variant("Go"), - betlang::Language::Gradle | betlang::Language::Groovy => { - language_spec_for_variant("Groovy") - } - betlang::Language::Haskell => language_spec_for_variant("Haskell"), - betlang::Language::Html => language_spec_for_variant("Html"), - betlang::Language::Ini => language_spec_for_variant("Ini"), - betlang::Language::Java => language_spec_for_variant("Java"), - betlang::Language::JavaScript => language_spec_for_variant("JavaScript"), - betlang::Language::Json => language_spec_for_variant("Json"), - betlang::Language::Julia => language_spec_for_variant("Julia"), - betlang::Language::Kotlin => language_spec_for_variant("Kotlin"), - betlang::Language::Lisp => language_spec_for_variant("CommonLisp"), - betlang::Language::Lua => language_spec_for_variant("Lua"), - betlang::Language::Markdown => language_spec_for_variant("Markdown"), - betlang::Language::ObjectiveC => language_spec_for_variant("ObjectiveC"), - betlang::Language::Ocaml => language_spec_for_variant("OCaml"), - betlang::Language::Perl => language_spec_for_variant("Perl"), - betlang::Language::Php => language_spec_for_variant("Php"), - betlang::Language::Powershell => language_spec_for_variant("PowerShell"), - betlang::Language::Python => language_spec_for_variant("Python"), - betlang::Language::R => language_spec_for_variant("R"), - betlang::Language::Rust => language_spec_for_variant("Rust"), - betlang::Language::Scala => language_spec_for_variant("Scala"), - betlang::Language::Shell => language_spec_for_variant("Bash"), - betlang::Language::Sql => language_spec_for_variant("Sql"), - betlang::Language::Swift => language_spec_for_variant("Swift"), - betlang::Language::Toml => language_spec_for_variant("Toml"), - betlang::Language::TypeScript => language_spec_for_variant("TypeScript"), - betlang::Language::Vba => language_spec_for_variant("VisualBasic"), - betlang::Language::Verilog => language_spec_for_variant("Verilog"), - betlang::Language::Xml => language_spec_for_variant("Xml"), - betlang::Language::Yaml => language_spec_for_variant("Yaml"), - _ => None, - } + let language = match language { + betlang::Language::Asm => "Asm", + betlang::Language::Batch => "Batch", + betlang::Language::C => "C", + betlang::Language::Clojure => "Clojure", + betlang::Language::CMake => "CMake", + betlang::Language::Cobol => "Cobol", + betlang::Language::Cpp => "Cpp", + betlang::Language::Cs => "CSharp", + betlang::Language::Css => "Css", + betlang::Language::Dart => "Dart", + betlang::Language::Dockerfile => "Dockerfile", + betlang::Language::Elixir => "Elixir", + betlang::Language::Erlang => "Erlang", + betlang::Language::Gemfile | betlang::Language::Gemspec | betlang::Language::Ruby => "Ruby", + betlang::Language::Go => "Go", + betlang::Language::Gradle | betlang::Language::Groovy => "Groovy", + betlang::Language::Haskell => "Haskell", + betlang::Language::Html => "Html", + betlang::Language::Ini => "Ini", + betlang::Language::Java => "Java", + betlang::Language::JavaScript => "JavaScript", + betlang::Language::Json => "Json", + betlang::Language::Julia => "Julia", + betlang::Language::Kotlin => "Kotlin", + betlang::Language::Lisp => "CommonLisp", + betlang::Language::Lua => "Lua", + betlang::Language::Markdown => "Markdown", + betlang::Language::ObjectiveC => "ObjectiveC", + betlang::Language::Ocaml => "OCaml", + betlang::Language::Perl => "Perl", + betlang::Language::Php => "Php", + betlang::Language::Powershell => "PowerShell", + betlang::Language::Python => "Python", + betlang::Language::R => "R", + betlang::Language::Rust => "Rust", + betlang::Language::Scala => "Scala", + betlang::Language::Shell => "Bash", + betlang::Language::Sql => "Sql", + betlang::Language::Swift => "Swift", + betlang::Language::Toml => "Toml", + betlang::Language::TypeScript => "TypeScript", + betlang::Language::Vba => "VisualBasic", + betlang::Language::Verilog => "Verilog", + betlang::Language::Xml => "Xml", + betlang::Language::Yaml => "Yaml", + _ => return None, + }; + language_spec_for_variant(language) } fn expand_code(input: CodeInput) -> syn::Result { From 413530c11899fa5edc6a38c334f36c0b59bf3bba Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 May 2026 09:19:37 -0500 Subject: [PATCH 14/15] type state builder --- src/lib.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3a80366..48d3605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,7 @@ pub use advanced::{HighlightError, HighlightQueryErrorKind}; /// Source text to highlight at runtime. /// /// Available with the `runtime` feature. Build one with [`SourceCode::new`], -/// then pass it to [`Code()`]. +/// or with [`SourceCode::builder`], then pass it to [`Code()`]. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; @@ -230,11 +230,39 @@ pub struct SourceCode { } /// Source-first builder for [`SourceCode`]. +/// +/// Call [`SourceCodeBuilder::with_language`] to set the language, then +/// `build()` to produce [`SourceCode`]. With the `detection` feature enabled, +/// `build()` may also be called without an explicit language to detect from +/// the source text. +/// +/// ```rust +/// use dioxus_code::{Language, SourceCode}; +/// +/// let _src = SourceCode::builder("fn main() {}") +/// .with_language(Language::Rust) +/// .build(); +/// ``` #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] #[derive(Debug, Clone, PartialEq, Eq)] -pub struct SourceCodeBuilder { +pub struct SourceCodeBuilder { source: String, + state: State, +} + +/// Builder state before a [`SourceCodeBuilder`] has a language. +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceCodeBuilderMissingLanguage; + +/// Builder state after a [`SourceCodeBuilder`] has a language. +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceCodeBuilderWithLanguage { + language: Language, } #[cfg(feature = "runtime")] @@ -257,6 +285,7 @@ impl SourceCode { pub fn builder(source: impl ToString) -> SourceCodeBuilder { SourceCodeBuilder { source: source.to_string(), + state: SourceCodeBuilderMissingLanguage, } } @@ -294,10 +323,38 @@ impl SourceCode { #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] -impl SourceCodeBuilder { - /// Finish the builder with an explicit language. - pub fn with_language(self, language: Language) -> SourceCode { - SourceCode::new(language, self.source) +impl SourceCodeBuilder { + /// Set the language that will be used to highlight this source. + pub fn with_language( + self, + language: Language, + ) -> SourceCodeBuilder { + SourceCodeBuilder { + source: self.source, + state: SourceCodeBuilderWithLanguage { language }, + } + } + + /// Finish the builder, detecting the language from the source text. + #[cfg(feature = "detection")] + #[cfg_attr(docsrs, doc(cfg(feature = "detection")))] + pub fn build(self) -> SourceCode { + SourceCode::new(Language::Auto, self.source) + } +} + +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +impl SourceCodeBuilder { + /// Replace the language that will be used to highlight this source. + pub fn with_language(mut self, language: Language) -> Self { + self.state.language = language; + self + } + + /// Finish the builder. + pub fn build(self) -> SourceCode { + SourceCode::new(self.state.language, self.source) } } @@ -501,6 +558,25 @@ mod tests { })); } + #[cfg(feature = "runtime")] + #[test] + fn source_code_builder_builds_after_language_is_set() { + let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}") + .with_language(Language::Rust) + .build() + .into(); + assert_eq!(tree.language(), Language::Rust); + assert_eq!(tree.source(), "fn main() {}"); + } + + #[cfg(feature = "detection")] + #[test] + fn source_code_builder_can_detect_language_when_enabled() { + let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}").build().into(); + assert_eq!(tree.language(), Language::Rust); + assert_eq!(tree.source(), "fn main() {}"); + } + #[cfg(feature = "macro")] #[test] fn code_str_macro_highlights_inline_source() { From 96f28f647b0d36b7d7ee2fe22a8db8657063f45d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 19 May 2026 09:39:41 -0500 Subject: [PATCH 15/15] bump versions --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- code-editor/Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b50ae77..c88b377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2398,7 +2398,7 @@ dependencies = [ [[package]] name = "dioxus-code" -version = "0.1.2" +version = "0.1.3" dependencies = [ "arborium", "arborium-theme", @@ -2412,7 +2412,7 @@ dependencies = [ [[package]] name = "dioxus-code-basic" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2420,7 +2420,7 @@ dependencies = [ [[package]] name = "dioxus-code-docsite" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2430,7 +2430,7 @@ dependencies = [ [[package]] name = "dioxus-code-editor" -version = "0.1.3" +version = "0.1.4" dependencies = [ "dioxus", "dioxus-code", @@ -2440,7 +2440,7 @@ dependencies = [ [[package]] name = "dioxus-code-editor-example" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2449,7 +2449,7 @@ dependencies = [ [[package]] name = "dioxus-code-live-input" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "dioxus-code-macro" -version = "0.1.1" +version = "0.1.2" dependencies = [ "arborium", "arborium-theme", @@ -2472,7 +2472,7 @@ dependencies = [ [[package]] name = "dioxus-code-macro-only" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", diff --git a/Cargo.toml b/Cargo.toml index 64e5c8c..740faa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ resolver = "3" [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2024" license = "MIT" repository = "https://github.com/ealmloff/dioxus-code" @@ -26,7 +26,7 @@ dioxus-code-editor = { version = "0.1.2", path = "code-editor", default-features [package] name = "dioxus-code" -version = "0.1.2" +version = "0.1.3" edition.workspace = true license.workspace = true description = "Syntax-highlighted code blocks for Dioxus." diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 96535cc..9ba8b99 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-code-editor" -version = "0.1.3" +version = "0.1.4" edition.workspace = true license.workspace = true description = "Syntax-highlighted code editor component for Dioxus."