use std::sync::atomic::{AtomicU64, Ordering};
use dioxus::{
document::{eval, Eval},
prelude::*,
};
use futures::StreamExt;
use crate::{
cmd::Cmd,
evt::Evt,
language::Language,
lsp::{lsp_bridge::LspBridge, lsp_message::LspMessage},
theme::Theme,
};
static EDITOR_ID_NEXT: AtomicU64 = AtomicU64::new(0);
const CM_ASSETS: Asset = asset!("/assets/codemirror-vendor", AssetOptions::folder());
#[derive(Props, Clone, PartialEq)]
pub struct CodeMirrorProps {
pub value: Signal<String>,
#[props(default)]
pub allow_multiple_selections: bool,
#[props(default)]
pub bracket_matching: bool,
#[props(default)]
pub close_brackets: bool,
#[props(default)]
pub highlight_active_line: bool,
#[props(default)]
pub highlight_selection_matches: bool,
#[props(default)]
pub highlight_whitespace: bool,
#[props(default)]
pub indent_on_input: bool,
#[props(default)]
pub indent_with_tab: bool,
#[props(default)]
pub language: Option<Language>,
#[props(default)]
pub line_numbers: bool,
#[props(default)]
pub line_wrapping: bool,
#[props(default)]
pub lsp: Option<LspBridge>,
#[props(default)]
pub on_ready: Option<EventHandler<()>>,
#[props(default)]
pub read_only: bool,
#[props(default)]
pub rectangular_selection: bool,
#[props(default)]
pub tab_size: Option<u8>,
#[props(default)]
pub theme: Theme,
}
#[component]
pub fn CodeMirror(props: CodeMirrorProps) -> Element {
let CodeMirrorProps {
mut value,
allow_multiple_selections,
bracket_matching,
close_brackets,
highlight_active_line,
highlight_selection_matches,
highlight_whitespace,
indent_on_input,
indent_with_tab,
language,
line_numbers,
line_wrapping,
lsp,
on_ready,
read_only,
rectangular_selection,
tab_size,
theme,
} = props;
let mount_id = use_hook(|| {
format!(
"cm-editor-{}",
EDITOR_ID_NEXT.fetch_add(1, Ordering::Relaxed)
)
});
let mut eval_handle = use_signal(|| None::<Eval>);
let mut doc_synced = use_signal(String::new);
let mount_id_future = mount_id.clone();
let lsp_push = lsp.clone();
use_future(move || {
let mount_id = mount_id_future.clone();
let lsp = lsp.clone();
async move {
let mut evaluator = eval(include_str!("code_mirror/glue.js"));
let init = Cmd::Init {
mount_id,
cm_base: CM_ASSETS.to_string(),
doc: value.peek().clone(),
allow_multiple_selections,
bracket_matching,
close_brackets,
highlight_active_line,
highlight_selection_matches,
highlight_whitespace,
indent_on_input,
indent_with_tab,
language,
line_numbers,
line_wrapping,
lsp_uri: lsp.as_ref().map(|lsp| lsp.uri.clone()),
read_only,
rectangular_selection,
tab_size,
};
if evaluator.send(init).is_err() {
return;
}
doc_synced.set(value.peek().clone());
eval_handle.set(Some(evaluator));
loop {
match evaluator.recv::<Evt>().await {
Ok(Evt::Ready) => {
if let Some(on_ready) = on_ready.as_ref() {
on_ready.call(());
}
}
Ok(Evt::DocChanged { doc }) => {
doc_synced.set(doc.clone());
value.set(doc);
}
Ok(Evt::LspMessageRecv { json }) => {
if let Some(lsp) = lsp.as_ref() {
let replies = lsp.on_message_to_server.call(LspMessage::new(json));
for reply in replies {
let _ = evaluator.send(Cmd::LspMessageSend {
json: reply.json_into(),
});
}
}
}
Err(_) => break,
}
}
}
});
use_future(move || {
let lsp_push = lsp_push.clone();
async move {
let Some(mut messages_pushed_rx) = lsp_push
.as_ref()
.and_then(LspBridge::messages_pushed_rx_take)
else {
return;
};
while let Some(message) = messages_pushed_rx.next().await {
if let Some(evaluator) = eval_handle.peek().as_ref() {
let _ = evaluator.send(Cmd::LspMessageSend {
json: message.json_into(),
});
}
}
}
});
use_effect(move || {
let value_current = value.read().clone();
if value_current != *doc_synced.peek()
&& let Some(evaluator) = eval_handle.peek().as_ref()
&& evaluator
.send(Cmd::DocSet {
doc: value_current.clone(),
})
.is_ok()
{
doc_synced.set(value_current);
}
});
rsx! {
div {
id: "{mount_id}",
class: "dioxus-codemirror",
"data-theme": theme.theme_attr(),
}
}
}