use std::rc::Rc; use anyhow::Error; use itertools::Itertools; use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen}; use wasm_bindgen_futures::js_sys::Object; use web_sys::{window, HtmlElement}; use yew::{ function_component, html, use_effect_with, use_node_ref, use_state, use_state_eq, AttrValue, Callback, Html, Properties, UseStateHandle, }; use crate::{FrameId, GlobalLayoutState, GlobalMemoCell, PanelDirection, SplitPanel}; #[derive(Properties, PartialEq, Clone)] pub struct EditorViewProps { pub frame: FrameId, pub global_memo: GlobalMemoCell, pub global_layout: UseStateHandle>, } #[derive(Properties, PartialEq, Clone)] pub struct EditorViewDetailProps { pub frame: FrameId, pub global_memo: GlobalMemoCell, pub global_layout: UseStateHandle>, pub editor_state: UseStateHandle>, } fn close_editor(frame: &FrameId, global_layout: &UseStateHandle>) { let mut gl_new: GlobalLayoutState = global_layout.as_ref().clone(); gl_new.frame_views.remove(frame); global_layout.set(gl_new.into()); } #[derive(Clone, PartialEq)] pub struct EditorViewState { error_msg: Option, available_files: Vec, open_file: AttrValue, } pub struct EditorKeepClosures { change_closure: Rc>, } fn fetch_initial_editor_state_or_fail() -> anyhow::Result { let win = window().ok_or_else(|| Error::msg("Can't get window"))?; let local = win .local_storage() .map_err(|_| Error::msg("Error retrieving localStorage"))? .ok_or_else(|| Error::msg("Local storage not available"))?; let n_keys = local .length() .map_err(|_| Error::msg("localStorage broken"))?; let mut available_files: Vec = vec![]; for i in 0..n_keys { if let Some(key) = local .key(i) .map_err(|_| Error::msg("localStorage broken"))? { match key.strip_prefix("scriptfile_") { None => {} Some(filename) => { available_files.push(filename.to_owned().into()); } } } } let init_str: AttrValue = "init.lua".into(); if !available_files.contains(&init_str) { available_files.push(init_str); local .set( "scriptfile_init.lua", "-- Put any code to run on every client load here.\n", ) .map_err(|_| Error::msg("localStorage broken"))?; } Ok(EditorViewState { error_msg: None, available_files, open_file: "init.lua".into(), }) } fn fetch_initial_editor_state() -> Rc { match fetch_initial_editor_state_or_fail() { Ok(s) => s.into(), Err(e) => EditorViewState { error_msg: Some(format!("Can't load editor data: {}", e).into()), available_files: vec![], open_file: "init.lua".into(), } .into(), } } pub fn try_run_script(script: &str, global_memo: &GlobalMemoCell) -> anyhow::Result<()> { let script = load_file_contents(script)?; global_memo .lua_engine .borrow_mut() .execute(&script) .map_err(Error::msg)?; Ok(()) } fn run_script(script: &str, global_memo: &GlobalMemoCell, set_err: Rc) where F: Fn(Option<&str>), { match try_run_script(script, global_memo) { Ok(()) => { set_err(None); } Err(e) => { set_err(Some(&format!("Script error: {}", e))); } } } #[function_component(EditorNav)] fn editor_nav(props: &EditorViewDetailProps) -> Html { let global_memo = props.global_memo.clone(); let global_layout = props.global_layout.clone(); let frame = props.frame.clone(); let current_script = props.editor_state.open_file.clone(); let set_err_state = props.editor_state.clone(); let set_err = Rc::new(move |msg: Option<&str>| { let mut new_state = (*set_err_state.as_ref()).clone(); new_state.error_msg = msg.map(|v| AttrValue::from(String::from(v))); set_err_state.set(new_state.into()); }); html! {
    {props .editor_state .available_files .iter() .map(|f| { let mut classes = vec!["list-group-item"]; let mut aria_current = None; if *f == props.editor_state.open_file { aria_current = Some("true"); classes.push("active"); } html! {
  • {f}
  • } }) .collect::>() }
} } #[wasm_bindgen(getter_with_clone)] struct EditorViewConfig { pub doc: String, } #[wasm_bindgen(getter_with_clone)] struct EditorStateConfig { pub doc: String, pub extensions: Vec, } #[wasm_bindgen(getter_with_clone)] struct HighlightingConfig { pub fallback: bool, } #[wasm_bindgen(module = "@codemirror/state")] extern "C" { type EditorState; #[wasm_bindgen(extends = Object)] type CMDocument; #[wasm_bindgen(static_method_of = EditorState)] fn create(settings: EditorStateConfig) -> EditorState; #[wasm_bindgen(method, getter)] fn doc(st: &EditorState) -> CMDocument; #[wasm_bindgen(js_namespace = ["EditorState", "allowMultipleSelections"], js_name = of)] fn editorstate_allow_multiple_selections_of(v: bool) -> CMExtension; } #[wasm_bindgen(module = "@codemirror/view")] extern "C" { type EditorView; type ViewUpdate; #[derive(Clone)] type KeyBinding; #[derive(Clone)] type CMExtension; #[wasm_bindgen(constructor)] fn new(settings: EditorViewConfig) -> EditorView; #[wasm_bindgen(js_namespace = ["EditorView", "updateListener"], js_name = of)] fn editorview_updatelistener_of(cb: &Closure) -> CMExtension; #[wasm_bindgen(method, getter)] fn dom(upd: &EditorView) -> HtmlElement; #[wasm_bindgen(method, getter)] fn state(v: &EditorView) -> EditorState; #[wasm_bindgen(method, js_name = setState)] fn set_state(v: &EditorView, st: &EditorState); #[wasm_bindgen(method, getter, js_name = docChanged)] fn doc_changed(upd: &ViewUpdate) -> bool; #[wasm_bindgen(method, getter)] fn view(upd: &ViewUpdate) -> EditorView; #[wasm_bindgen(js_name = highlightSpecialChars)] fn highlight_special_chars() -> CMExtension; #[wasm_bindgen(js_name = drawSelection)] fn draw_selection() -> CMExtension; #[wasm_bindgen(js_name = highlightActiveLine)] fn highlight_active_line() -> CMExtension; #[wasm_bindgen(js_name = dropCursor)] fn drop_cursor() -> CMExtension; #[wasm_bindgen(js_name = rectangularSelection)] fn rectangular_selection() -> CMExtension; #[wasm_bindgen(js_name = crosshairCursor)] fn crosshair_cursor() -> CMExtension; #[wasm_bindgen(js_name = lineNumbers)] fn line_numbers() -> CMExtension; #[wasm_bindgen(js_name = highlightActiveLineGutter)] fn highlight_active_line_gutter() -> CMExtension; #[wasm_bindgen(js_namespace = keymap, js_name = of)] fn keymap_of(keys: Vec) -> CMExtension; } #[wasm_bindgen(module = "@codemirror/legacy-modes/mode/lua")] extern "C" { #[wasm_bindgen(js_name = lua, thread_local)] static LUA: StreamLanguage; } #[wasm_bindgen(module = "@codemirror/language")] extern "C" { #[derive(Clone)] type StreamLanguage; #[derive(Clone)] type CMHighlightStyle; #[wasm_bindgen(js_namespace = StreamLanguage, js_name = define)] fn streamlanguage_define(input: StreamLanguage) -> CMExtension; #[wasm_bindgen(js_name = defaultHighlightStyle, thread_local)] static DEFAULT_HIGHLIGHT_STYLE: CMHighlightStyle; #[wasm_bindgen(js_name = syntaxHighlighting)] fn syntax_highlighting(style: CMHighlightStyle, config: HighlightingConfig) -> CMExtension; #[wasm_bindgen(js_name = indentOnInput)] fn indent_on_input() -> CMExtension; #[wasm_bindgen(js_name = bracketMatching)] fn bracket_matching() -> CMExtension; #[wasm_bindgen(js_name = foldGutter)] fn fold_gutter() -> CMExtension; #[wasm_bindgen(js_name = foldKeymap, thread_local)] static FOLD_KEYMAP: Vec; } #[wasm_bindgen(module = "@codemirror/commands")] extern "C" { #[wasm_bindgen(js_name = defaultKeymap, thread_local)] static DEFAULT_KEYMAP: Vec; #[wasm_bindgen(js_name = historyKeymap, thread_local)] static HISTORY_KEYMAP: Vec; #[wasm_bindgen] fn history() -> CMExtension; } #[wasm_bindgen(module = "@codemirror/autocomplete")] extern "C" { #[wasm_bindgen] fn autocompletion() -> CMExtension; #[wasm_bindgen(js_name = closeBrackets)] fn close_brackets() -> CMExtension; #[wasm_bindgen(js_name = completionKeymap, thread_local)] static COMPLETION_KEYMAP: Vec; #[wasm_bindgen(js_name = closeBracketsKeymap, thread_local)] static CLOSE_BRACKETS_KEYMAP: Vec; } #[wasm_bindgen(module = "@codemirror/search")] extern "C" { #[wasm_bindgen(js_name = searchKeymap, thread_local)] static SEARCH_KEYMAP: Vec; #[wasm_bindgen(js_name = highlightSelectionMatches)] fn highlight_selection_matches() -> CMExtension; } #[wasm_bindgen(module = "@codemirror/lint")] extern "C" { #[wasm_bindgen(js_name = lintKeymap, thread_local)] static LINT_KEYMAP: Vec; } fn try_save_document(file: &str, contents: &str) -> anyhow::Result<()> { let win = window().ok_or_else(|| Error::msg("Can't get window"))?; let local = win .local_storage() .map_err(|_| Error::msg("Error retrieving localStorage"))? .ok_or_else(|| Error::msg("Local storage not available"))?; local .set(&format!("scriptfile_{}", file), contents) .map_err(|_| Error::msg("Error saving to localStorage")) } fn load_file_contents(file: &str) -> anyhow::Result { let win = window().ok_or_else(|| Error::msg("Can't get window"))?; let local = win .local_storage() .map_err(|_| Error::msg("Error retrieving localStorage"))? .ok_or_else(|| Error::msg("Local storage not available"))?; Ok(local .get(&format!("scriptfile_{}", file)) .map_err(|_| Error::msg("Error retrieving localStorage"))? .unwrap_or_else(|| "".to_owned())) } #[function_component(EditorArea)] fn editor_area(props: &EditorViewDetailProps) -> Html { let node_ref = use_node_ref(); let editor_state = props.editor_state.clone(); let closures = use_state(|| EditorKeepClosures { change_closure: Closure::new(move |view_update: ViewUpdate| { if view_update.doc_changed() { if let Some(contents) = view_update.view().state().doc().to_string().as_string() { match try_save_document(&editor_state.open_file, &contents) { Ok(()) => {} Err(e) => { let mut new_state = (*editor_state.as_ref()).clone(); new_state.error_msg = Some(format!("Can't save: {}", e).into()); editor_state.set(new_state.into()); } } } } }) .into(), }); let view = use_state(move || { EditorView::new(EditorViewConfig { doc: "Loading...".to_owned(), }) }); { let view = view.clone(); use_effect_with(props.editor_state.open_file.clone(), move |open_file| { view.set_state(&EditorState::create(EditorStateConfig { doc: load_file_contents(open_file) .unwrap_or_else(|e| format!("Error loading content: {}", e.to_string())), extensions: vec![ // BASIC_SETUP.with(CMExtension::clone), line_numbers(), highlight_active_line_gutter(), highlight_special_chars(), history(), fold_gutter(), draw_selection(), drop_cursor(), editorstate_allow_multiple_selections_of(true), indent_on_input(), syntax_highlighting( DEFAULT_HIGHLIGHT_STYLE.with(CMHighlightStyle::clone), HighlightingConfig { fallback: true }, ), bracket_matching(), close_brackets(), autocompletion(), rectangular_selection(), crosshair_cursor(), highlight_active_line(), highlight_selection_matches(), keymap_of( vec![ &CLOSE_BRACKETS_KEYMAP, &DEFAULT_KEYMAP, &SEARCH_KEYMAP, &HISTORY_KEYMAP, &FOLD_KEYMAP, &COMPLETION_KEYMAP, &LINT_KEYMAP, ] .into_iter() .flat_map(|k| k.with(|v| v.clone()).into_iter()) .collect(), ), StreamLanguage::streamlanguage_define(LUA.with(StreamLanguage::clone)), editorview_updatelistener_of(&closures.change_closure), ], })); }); } use_effect_with(node_ref.clone(), move |node_ref| match node_ref.get() { None => {} Some(node) => { let _ = node.append_child(&view.dom()); } }); html! {
} } #[derive(Properties, PartialEq, Clone)] pub struct ErrorBarProps { pub msg: AttrValue, pub dismiss: Callback<()>, } #[function_component(ErrorBar)] fn error_bar(props: &ErrorBarProps) -> Html { let props = props.clone(); html! { } } #[function_component(CodeEditorView)] pub fn editor_view(props: &EditorViewProps) -> Html { let editor_state: UseStateHandle> = use_state_eq(fetch_initial_editor_state); let detail_props: EditorViewDetailProps = EditorViewDetailProps { frame: props.frame.clone(), global_memo: props.global_memo.clone(), global_layout: props.global_layout.clone(), editor_state: editor_state.clone(), }; html! { <> {match editor_state.error_msg.as_ref() { None => { html! { <> }} Some(msg) => { let editor_state = editor_state.clone(); html! { } } }} }} second={html!{}} /> } }