#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
use dioxus::prelude::*;
pub use dioxus_code::Language;
#[cfg(test)]
use dioxus_code::Theme;
use dioxus_code::advanced::{Buffer, CodeThemeStyles, HighlightError, TokenSpan};
#[cfg(test)]
use dioxus_code::advanced::{HighlightSegment, HighlightedSource};
use dioxus_code::{CodeTheme, SourceCode};
use std::{cell::RefCell, rc::Rc};
mod edit_capture;
pub const CODE_EDITOR_CSS: Asset = asset!("/assets/dioxus-code-editor.css");
#[derive(Props, Clone, PartialEq)]
pub struct CodeEditorProps {
#[props(into)]
pub value: String,
#[props(default = Language::Rust)]
pub language: Language,
#[props(default, into)]
pub theme: CodeTheme,
#[props(default = true)]
pub line_numbers: bool,
#[props(default = false)]
pub read_only: bool,
#[props(default = false)]
pub spellcheck: bool,
#[props(into, default = "Code editor")]
pub aria_label: String,
#[props(into, default)]
pub placeholder: String,
#[props(into, default)]
pub class: String,
#[props(default = EventHandler::new(|_: String| {}))]
pub oninput: EventHandler<String>,
}
struct EditorBuffer {
buffer: Option<Buffer>,
language: Language,
}
#[component]
pub fn CodeEditor(props: CodeEditorProps) -> Element {
let state = use_hook({
let value = props.value.clone();
let language = props.language;
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(
props.value.clone(),
)))
});
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;
}
match slot.buffer.as_mut() {
Some(buffer) => {
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),
},
None => buffer.replace(props.value.clone()),
};
let _ = result;
}
buffer.highlighted()
}
None => SourceCode::new(props.language, props.value.clone()).into(),
}
};
let lines = snapshot.lines();
let line_count = lines.len();
let class = editor_class(props.theme, props.line_numbers, &props.class);
let textarea_value = props.value.clone();
let readonly = props.read_only.then_some("true");
let input_attributes = edit_capture::use_input_edit_attributes(edit_tracker.clone(), {
let oninput = props.oninput;
move |value| oninput.call(value)
});
rsx! {
CodeThemeStyles { theme: props.theme }
document::Stylesheet { href: CODE_EDITOR_CSS }
div {
class,
if props.line_numbers {
div { class: "dxc-editor-gutter", aria_hidden: "true",
for index in 0..line_count {
div { class: "dxc-editor-gutter-line", "{index + 1}" }
}
}
}
div { class: "dxc-editor-viewport",
div { class: "dxc-editor-highlight", aria_hidden: "true",
for line in lines {
div { class: "dxc-editor-line",
for segment in line {
if let Some(tag) = segment.tag() {
TokenSpan {
text: segment.text(),
tag,
}
} else {
span { "{segment.text()}" }
}
}
}
}
}
textarea {
class: "dxc-editor-input",
readonly: props.read_only,
spellcheck: props.spellcheck,
role: "textbox",
"aria-label": props.aria_label,
"aria-multiline": "true",
"aria-readonly": readonly,
placeholder: props.placeholder,
wrap: "off",
..input_attributes,
"{textarea_value}"
}
}
}
}
}
fn editor_class(theme: impl Into<CodeTheme>, line_numbers: bool, extra_class: &str) -> String {
let mut class = format!("dxc-editor {}", theme.into().classes());
if !line_numbers {
class.push_str(" dxc-editor-no-gutter");
}
if !extra_class.is_empty() {
class.push(' ');
class.push_str(extra_class);
}
class
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn editor_class_includes_theme_and_extra_class() {
assert_eq!(
editor_class(Theme::TOKYO_NIGHT, true, "is-compact"),
"dxc-editor dxc-tokyo-night is-compact",
);
}
#[test]
fn editor_class_can_hide_gutter() {
assert_eq!(
editor_class(Theme::TOKYO_NIGHT, false, ""),
"dxc-editor dxc-tokyo-night dxc-editor-no-gutter",
);
}
#[test]
fn editor_class_can_use_system_theme() {
assert_eq!(
editor_class(
CodeTheme::system(Theme::GITHUB_LIGHT, Theme::TOKYO_NIGHT),
true,
"",
),
"dxc-editor dxc-system dxc-system-light-github-light dxc-system-dark-tokyo-night",
);
}
#[test]
fn lines_preserve_trailing_empty_line() {
let source = HighlightedSource::from_static_parts("let x = 1;\n", Language::Rust, &[]);
let lines = source.lines();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], vec![HighlightSegment::new("let x = 1;", None)]);
assert!(lines[1].is_empty());
}
}