2024-10-05 22:54:48 +10:00
|
|
|
use std::rc::Rc;
|
2024-10-05 19:28:39 +10:00
|
|
|
|
|
|
|
use anyhow::Error;
|
2024-10-05 22:54:48 +10:00
|
|
|
use itertools::Itertools;
|
|
|
|
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen};
|
|
|
|
use wasm_bindgen_futures::js_sys::Object;
|
|
|
|
use web_sys::{window, HtmlElement};
|
2024-10-05 19:28:39 +10:00
|
|
|
use yew::{
|
2024-10-05 22:54:48 +10:00
|
|
|
function_component, html, use_effect_with, use_node_ref, use_state, use_state_eq, AttrValue,
|
|
|
|
Callback, Html, Properties, UseStateHandle,
|
2024-10-05 19:28:39 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
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<Rc<GlobalLayoutState>>,
|
|
|
|
}
|
|
|
|
|
2024-10-05 22:54:48 +10:00
|
|
|
#[derive(Properties, PartialEq, Clone)]
|
|
|
|
pub struct EditorViewDetailProps {
|
|
|
|
pub frame: FrameId,
|
|
|
|
pub global_memo: GlobalMemoCell,
|
|
|
|
pub global_layout: UseStateHandle<Rc<GlobalLayoutState>>,
|
|
|
|
pub editor_state: UseStateHandle<Rc<EditorViewState>>,
|
|
|
|
}
|
|
|
|
|
2024-10-05 19:28:39 +10:00
|
|
|
fn close_editor(frame: &FrameId, global_layout: &UseStateHandle<Rc<GlobalLayoutState>>) {
|
|
|
|
let mut gl_new: GlobalLayoutState = global_layout.as_ref().clone();
|
|
|
|
gl_new.frame_views.remove(frame);
|
|
|
|
global_layout.set(gl_new.into());
|
|
|
|
}
|
|
|
|
|
2024-10-05 22:54:48 +10:00
|
|
|
#[derive(Clone, PartialEq)]
|
|
|
|
pub struct EditorViewState {
|
2024-10-05 19:28:39 +10:00
|
|
|
error_msg: Option<AttrValue>,
|
|
|
|
available_files: Vec<AttrValue>,
|
2024-10-05 22:54:48 +10:00
|
|
|
open_file: AttrValue,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct EditorKeepClosures {
|
|
|
|
change_closure: Rc<Closure<dyn FnMut(ViewUpdate)>>,
|
2024-10-05 19:28:39 +10:00
|
|
|
}
|
|
|
|
|
2024-10-05 22:54:48 +10:00
|
|
|
fn fetch_initial_editor_state_or_fail() -> anyhow::Result<EditorViewState> {
|
2024-10-05 19:28:39 +10:00
|
|
|
let win = window().ok_or_else(|| Error::msg("Can't get window"))?;
|
2024-10-05 22:54:48 +10:00
|
|
|
let local = win
|
|
|
|
.local_storage()
|
2024-10-05 19:28:39 +10:00
|
|
|
.map_err(|_| Error::msg("Error retrieving localStorage"))?
|
|
|
|
.ok_or_else(|| Error::msg("Local storage not available"))?;
|
2024-10-05 22:54:48 +10:00
|
|
|
|
|
|
|
let n_keys = local
|
|
|
|
.length()
|
|
|
|
.map_err(|_| Error::msg("localStorage broken"))?;
|
|
|
|
let mut available_files: Vec<AttrValue> = 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 {
|
2024-10-05 19:28:39 +10:00
|
|
|
error_msg: None,
|
2024-10-05 22:54:48 +10:00
|
|
|
available_files,
|
|
|
|
open_file: "init.lua".into(),
|
2024-10-05 19:28:39 +10:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-10-05 22:54:48 +10:00
|
|
|
fn fetch_initial_editor_state() -> Rc<EditorViewState> {
|
2024-10-05 19:28:39 +10:00
|
|
|
match fetch_initial_editor_state_or_fail() {
|
2024-10-05 22:54:48 +10:00
|
|
|
Ok(s) => s.into(),
|
|
|
|
Err(e) => EditorViewState {
|
|
|
|
error_msg: Some(format!("Can't load editor data: {}", e).into()),
|
2024-10-05 19:28:39 +10:00
|
|
|
available_files: vec![],
|
2024-10-05 22:54:48 +10:00
|
|
|
open_file: "init.lua".into(),
|
|
|
|
}
|
|
|
|
.into(),
|
2024-10-05 19:28:39 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[function_component(EditorNav)]
|
2024-10-05 22:54:48 +10:00
|
|
|
fn editor_nav(props: &EditorViewDetailProps) -> Html {
|
2024-10-05 19:28:39 +10:00
|
|
|
let global_layout = props.global_layout.clone();
|
|
|
|
let frame = props.frame.clone();
|
|
|
|
html! {
|
|
|
|
<div class="editornav">
|
|
|
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
|
|
|
<button class="btn" aria-label="New file" title="New file"><i class="bi bi-file-earmark-plus"></i></button>
|
|
|
|
<div class="flex-fill"/>
|
|
|
|
<button class="btn" onclick={move |_ev| close_editor(&frame, &global_layout)} aria-label="Close Editor" title="Close editor"><i class="bi bi-arrow-return-left"></i></button>
|
|
|
|
</nav>
|
2024-10-05 22:54:48 +10:00
|
|
|
<ul class="p-2 list-group">
|
|
|
|
{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! { <li aria-current={aria_current} class={classes.iter().join(" ")}>{f}</li>}
|
|
|
|
})
|
|
|
|
.collect::<Vec<Html>>()
|
|
|
|
}
|
|
|
|
</ul>
|
2024-10-05 19:28:39 +10:00
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen(getter_with_clone)]
|
|
|
|
struct EditorViewConfig {
|
|
|
|
pub doc: String,
|
2024-10-05 22:54:48 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen(getter_with_clone)]
|
|
|
|
struct EditorStateConfig {
|
|
|
|
pub doc: String,
|
2024-10-05 19:28:39 +10:00
|
|
|
pub extensions: Vec<CMExtension>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[wasm_bindgen(module = codemirror)]
|
|
|
|
extern "C" {
|
|
|
|
#[derive(Clone)]
|
|
|
|
type CMExtension;
|
|
|
|
|
|
|
|
#[wasm_bindgen(constructor)]
|
|
|
|
fn new(settings: EditorViewConfig) -> EditorView;
|
|
|
|
|
|
|
|
#[wasm_bindgen(js_name = basicSetup, thread_local)]
|
|
|
|
static BASIC_SETUP: CMExtension;
|
|
|
|
}
|
|
|
|
|
2024-10-05 22:54:48 +10:00
|
|
|
#[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<dyn FnMut(ViewUpdate)>) -> 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;
|
|
|
|
}
|
|
|
|
|
2024-10-05 19:28:39 +10:00
|
|
|
#[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;
|
|
|
|
|
|
|
|
#[wasm_bindgen(js_namespace = StreamLanguage, js_name = define)]
|
|
|
|
fn streamlanguage_define(input: StreamLanguage) -> CMExtension;
|
|
|
|
}
|
|
|
|
|
2024-10-05 22:54:48 +10:00
|
|
|
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<String> {
|
|
|
|
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()))
|
|
|
|
}
|
|
|
|
|
2024-10-05 19:28:39 +10:00
|
|
|
#[function_component(EditorArea)]
|
2024-10-05 22:54:48 +10:00
|
|
|
fn editor_area(props: &EditorViewDetailProps) -> Html {
|
2024-10-05 19:28:39 +10:00
|
|
|
let node_ref = use_node_ref();
|
2024-10-05 22:54:48 +10:00
|
|
|
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())),
|
2024-10-05 19:28:39 +10:00
|
|
|
extensions: vec![
|
|
|
|
BASIC_SETUP.with(CMExtension::clone),
|
|
|
|
StreamLanguage::streamlanguage_define(LUA.with(StreamLanguage::clone)),
|
2024-10-05 22:54:48 +10:00
|
|
|
editorview_updatelistener_of(&closures.change_closure),
|
2024-10-05 19:28:39 +10:00
|
|
|
],
|
2024-10-05 22:54:48 +10:00
|
|
|
}));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
use_effect_with(node_ref.clone(), move |node_ref| match node_ref.get() {
|
|
|
|
None => {}
|
|
|
|
Some(node) => {
|
|
|
|
let _ = node.append_child(&view.dom());
|
2024-10-05 19:28:39 +10:00
|
|
|
}
|
|
|
|
});
|
|
|
|
html! {
|
|
|
|
<div class="editorarea" ref={node_ref.clone()}>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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! {
|
|
|
|
<div class="errorbar alert alert-danger alert-dismissible" role="alert">
|
|
|
|
<span class="errorbar-msg">{props.msg.clone()}</span>
|
|
|
|
<button class="btn-close" aria-label="Close" onclick={move |_ev| props.dismiss.emit(())}></button>
|
|
|
|
</div>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[function_component(CodeEditorView)]
|
|
|
|
pub fn editor_view(props: &EditorViewProps) -> Html {
|
2024-10-05 22:54:48 +10:00
|
|
|
let editor_state: UseStateHandle<Rc<EditorViewState>> =
|
|
|
|
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(),
|
|
|
|
};
|
2024-10-05 19:28:39 +10:00
|
|
|
|
|
|
|
html! {
|
|
|
|
<>
|
|
|
|
{match editor_state.error_msg.as_ref() {
|
|
|
|
None => { html! { <></> }}
|
2024-10-05 22:54:48 +10:00
|
|
|
Some(msg) => {
|
|
|
|
let editor_state = editor_state.clone();
|
|
|
|
html! {
|
2024-10-05 19:28:39 +10:00
|
|
|
<ErrorBar msg={msg.clone()}
|
2024-10-05 22:54:48 +10:00
|
|
|
dismiss={move |()| editor_state.set(EditorViewState {
|
2024-10-05 19:28:39 +10:00
|
|
|
error_msg: None,
|
2024-10-05 22:54:48 +10:00
|
|
|
..((*editor_state.as_ref()).clone())
|
|
|
|
}.into())
|
|
|
|
}/>
|
|
|
|
}
|
|
|
|
}
|
2024-10-05 19:28:39 +10:00
|
|
|
}}
|
|
|
|
<SplitPanel direction={PanelDirection::Horizontal}
|
2024-10-05 22:54:48 +10:00
|
|
|
first={html!{<EditorNav ..detail_props.clone() />}}
|
|
|
|
second={html!{<EditorArea ..detail_props.clone()/>}}
|
2024-10-05 19:28:39 +10:00
|
|
|
/>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
}
|