From 2d17816ef5e7f7e66a284178c8df4f971d6ee675 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 5 Oct 2024 22:54:48 +1000 Subject: [PATCH] Make editing init.lua possible. --- src/editor_view.rs | 241 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 206 insertions(+), 35 deletions(-) diff --git a/src/editor_view.rs b/src/editor_view.rs index 875eb82..4cafde0 100644 --- a/src/editor_view.rs +++ b/src/editor_view.rs @@ -1,11 +1,13 @@ -use std::{ops::Deref, rc::Rc}; +use std::rc::Rc; use anyhow::Error; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; -use web_sys::{window, Node}; +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_eq, AttrValue, Callback, - Html, Properties, UseStateHandle, + 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}; @@ -17,41 +19,88 @@ pub struct EditorViewProps { 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(PartialEq, Clone)] -pub struct EditorState { +#[derive(Clone, PartialEq)] +pub struct EditorViewState { error_msg: Option, available_files: Vec, + open_file: AttrValue, } -fn fetch_initial_editor_state_or_fail() -> anyhow::Result { +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"))?; - win.local_storage() + let local = win + .local_storage() .map_err(|_| Error::msg("Error retrieving localStorage"))? .ok_or_else(|| Error::msg("Local storage not available"))?; - Ok(EditorState { + + 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: vec![], + available_files, + open_file: "init.lua".into(), }) } -fn fetch_initial_editor_state() -> EditorState { +fn fetch_initial_editor_state() -> Rc { match fetch_initial_editor_state_or_fail() { - Ok(s) => s, - Err(e) => EditorState { - error_msg: Some(format!("Can't load editor data: {}", e.to_string()).into()), + 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(), } } #[function_component(EditorNav)] -fn editor_nav(props: &EditorViewProps) -> Html { +fn editor_nav(props: &EditorViewDetailProps) -> Html { let global_layout = props.global_layout.clone(); let frame = props.frame.clone(); html! { @@ -61,6 +110,23 @@ fn editor_nav(props: &EditorViewProps) -> 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::>() + } +
} } @@ -68,13 +134,16 @@ fn editor_nav(props: &EditorViewProps) -> Html { #[wasm_bindgen(getter_with_clone)] struct EditorViewConfig { pub doc: String, +} + +#[wasm_bindgen(getter_with_clone)] +struct EditorStateConfig { + pub doc: String, pub extensions: Vec, - pub parent: Node, } #[wasm_bindgen(module = codemirror)] extern "C" { - type EditorView; #[derive(Clone)] type CMExtension; @@ -85,6 +154,43 @@ extern "C" { static BASIC_SETUP: CMExtension; } +#[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(module = "@codemirror/view")] +extern "C" { + type EditorView; + type ViewUpdate; + + #[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(module = "@codemirror/legacy-modes/mode/lua")] extern "C" { #[wasm_bindgen(js_name = lua, thread_local)] @@ -100,20 +206,73 @@ extern "C" { fn streamlanguage_define(input: StreamLanguage) -> CMExtension; } +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: &EditorViewProps) -> Html { +fn editor_area(props: &EditorViewDetailProps) -> Html { let node_ref = use_node_ref(); - use_effect_with(node_ref.clone(), |node_ref| match node_ref.get() { - None => {} - Some(node) => { - EditorView::new(EditorViewConfig { - doc: "Hello World".to_owned(), + 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), StreamLanguage::streamlanguage_define(LUA.with(StreamLanguage::clone)), + editorview_updatelistener_of(&closures.change_closure), ], - parent: node, - }); + })); + }); + } + use_effect_with(node_ref.clone(), move |node_ref| match node_ref.get() { + None => {} + Some(node) => { + let _ = node.append_child(&view.dom()); } }); html! { @@ -141,23 +300,35 @@ fn error_bar(props: &ErrorBarProps) -> Html { #[function_component(CodeEditorView)] pub fn editor_view(props: &EditorViewProps) -> Html { - let editor_state: UseStateHandle = use_state_eq(fetch_initial_editor_state); + 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) => html! { + Some(msg) => { + let editor_state = editor_state.clone(); + html! { } + ..((*editor_state.as_ref()).clone()) + }.into()) + }/> + } + } }} }} - second={html!{}} + first={html!{}} + second={html!{}} /> }