diff --git a/README.md b/README.md index 58e9e49..1c46418 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,129 @@ -# Frobnicator (this is a template) +# CollabRustAtor ## Authors -- Andrzej Głuszak (@agluszak on GitHub) -- Linus Torvalds (@torvalds on GitHub) + +- Jan Wojtach (@YannVoytaa on GitHub) ## Description -Frobnicator is going to be a platformer game similar to Super Mario Bros made using Bevy game engine. + +CollabRustAtor is going to be a live collaborative rust code editor and compiler (web app, client-server architecture) ## Features -- map generator -- shooting -- enemy AI -- game state saving and loading -- scores + +- user can create a 'room' available for others, +- single room contains one text document editable by all room members simultaneously (potential feature- more advanced file structure: more files/directories) +- apart from editing the document, users can compile/run it as a rust file (potential feature- allowing for choosing programming languages other than rust) (for security reasons (infinite loops/...), there would be a time limit for a program to run) ## Plan -In the first part we're going to implement the basics: movement, physics and shooting. The enemies will simply bounce from one edge of the platform to the other. There will be only a single map. -In the second part we're going to add random map generator, saving/loading, scores and a more sophisticated AI. +In the first part I'm going to implement basic web-based rust code editor: no rooms, no concurrent editing, only editing and compiling/running would be available. + +In the second part I'm going to add: + +1. rooms- files can be saved to database (one file per one room), other users can see the changes after refreshing the page (or by clicking the button to reload the file content only- for better user experience); +2. concurrent editing- every client will be connected to WebSocket server and send/receive pieces of information about new file changes in certain room. ## Libraries -- Bevy -- Serde (for serialization) + +- Yew (client-side/frontend) +- Warp (server-side/backend) +- Serde (serialization) +- Syntect (syntax highlighting) +- Reqwest (communication between frontend and REST Api) +- Uuid (generating unique ids (for room ids)) +- log + env_logger (logging) +- redis (Redis client library) + +## Additional tech stack + +- Database (for now- Redis) + +## Running the application + +To run the application locally, you need Redis server up and running (the app stores each room's code piece inside a redis database). To install redis, you can check [this](https://redis.io/docs/getting-started/installation/) link. + +The code is divided into two parts- server and client part. + +To run the server side, go to the 'backend' directory and run + +``` +cargo run +``` + +To check if everyting is working, go to [localhost:8000/health/check](localhost:8000/health/check) (or any url localhost:8000/health/{String}) and check whether it returns 'check' (or the String value provided in the url other than 'check'). + +To run the client side, go to the 'frontend' directory and run + +``` +trunk serve +``` + +Then the website should be available under localhost:8080 (or any other free port; the information should be provided in the terminal after executing the 'serve' command). + +If the backend service is exposed under the port 8000, the project parts should communicate properly (that is, the code is saved for each room, it gets automatically updated for all room participants via Websockets + compiling/running it should produce a result). + +In other case (the address/port of the backend server is different than expected localhost:8000) the Trunk.toml file (located in the frontend/ directory) should be updated in order to establish communication between the two parts (the first proxy record with address "http://127.0.0.1:8000/" refers to REST Api and the second one- to the Websocket). + +It is also possible to host the project and make it public (to use it on multiple devices): + +- Thanks to the Trunk.toml file (which creates a Proxy for all the services with urls in the file) we just have to expose the port with the frontend part + +- One of the easiest solutions (and one which was used during testing/development) is to use ngrok + +- To do so, you can create a free-tier account [here](https://ngrok.com/) and then follow [this](https://ngrok.com/download) 3 step tutorial (install ngrok, add token, expose port (in our case port 8080, unless the frontend part started on another one))) + +- After exposing the port from your main device, you should see the url from which you can access the page from all devices (example url: https://8c5e-178-73-34-162.eu.ngrok.io) (it changes every time you expose a port via ngrok) + +## Example code snippets: + +``` +fn main() { + println!("abc"); +} +// shows 'abc' in the terminal as a result; +``` + +``` +fn main() { + println("abc"); +} +// gives an error with a standard explanation of a rust compiler; +``` + +``` +fn main() { + let a = 1; + println!("abc"); +} +// gives a warning, but shows 'abc' as a result as well; +``` + +``` +fn main() { + loop { + } +} +// results in timeout after 10 seconds. +``` + +``` +fn main() { + let mut x = 1; + loop { + x -= 1; + x /= x; + } +} +// results in a runtime error and 'panic' message. +``` + +## Useful resources + +These are the links to the websites that helped me build the current project and learn about the technologies used: + +- [Yew tutorial](https://yew.rs/docs/tutorial) +- [Building an API with Rust using Tokio and Warp](https://levelup.gitconnected.com/building-an-api-using-warp-and-tokio-26a52173860a) +- [Syntect docs and examples](https://github.com/trishume/syntect) +- [Redis library documentation](https://docs.rs/redis/latest/redis/) +- [Websocket server in rust using Warp](https://blog.logrocket.com/how-to-build-a-websocket-server-with-rust/) +- [Adding Websocket connection to yew frontend app](https://blog.devgenius.io/lets-build-a-websockets-project-with-rust-and-yew-0-19-60720367399f) diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..e906adc --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb +*.in +*.txt \ No newline at end of file diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..8ef096c --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.23.0", features = ["macros", "sync", "rt-multi-thread"] } +warp = "0.3" +serde = {version = "1.0", features = ["derive"] } +serde_json = "1.0" +futures = { version = "0.3" } +log = "0.4.17" +env_logger = "0.10.0" +redis = "0.21.5" \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..4a75fbf --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,90 @@ +use std::{collections::HashMap, sync::Arc}; + +use crate::run_code::code_handler; +use futures::{lock::Mutex, stream::SplitSink}; +use redis::Client; +use room::{current_room_code_handler, health_handler, room_handler}; +use serde::{Deserialize, Serialize}; +use warp::{ + self, + hyper::Method, + ws::{Message, WebSocket}, + Filter, +}; +pub mod room; +pub mod run_code; +#[derive(Clone, Serialize, Deserialize)] +pub struct Code { + code: String, +} + +async fn handle_rejection( + err: warp::Rejection, +) -> Result { + Ok(warp::reply::json(&format!("{:?}", err))) +} +pub type UsersInRoom = Vec>; + +#[tokio::main] +async fn main() { + env_logger::init(); + let cors = warp::cors() + .allow_any_origin() + .allow_headers(vec![ + "Access-Control-Allow-Headers", + "Access-Control-Request-Method", + "Access-Control-Request-Headers", + "Origin", + "Accept", + "X-Requested-With", + "content-type", + "Host", + "Referer", + "Accept", + "Content-Length", + ]) + .allow_methods(&[ + Method::GET, + Method::POST, + Method::PUT, + Method::PATCH, + Method::DELETE, + Method::OPTIONS, + Method::HEAD, + ]); + + let active_users: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + + let room_route = warp::path!("ws" / "room") + .and(warp::ws()) + .and(warp::any().map(|| redis::Client::open("redis://127.0.0.1").unwrap())) + .and(warp::any().map(move || active_users.clone())) + .and_then( + |ws: warp::ws::Ws, client: Client, active_users| async move { + room_handler(ws, &client, active_users).await + }, + ); + + let room_code_route = warp::path!("room" / String / "code") + .and(warp::any().map(|| redis::Client::open("redis://127.0.0.1").unwrap())) + .and_then(|key: String, client: Client| async move { + current_room_code_handler(key, &client).await + }); + + let health_route = warp::path!("health" / String).and_then(health_handler); + + let code_route = warp::path!("code" / String) + .and(warp::post()) + .and(warp::body::json()) + .and_then(code_handler); + + let routes = room_route + .or(room_code_route) + .or(code_route) + .or(health_route) + .recover(handle_rejection) + .with(&cors); + + warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +} diff --git a/backend/src/room.rs b/backend/src/room.rs new file mode 100644 index 0000000..eaa4bc7 --- /dev/null +++ b/backend/src/room.rs @@ -0,0 +1,94 @@ +use futures::lock::Mutex; +use log::info; +use std::collections::HashMap; +use std::sync::Arc; + +use futures::{SinkExt, StreamExt, TryStreamExt}; +use redis::Client; +use warp::{ws::Message, Rejection}; + +use crate::UsersInRoom; +pub async fn current_room_code_handler(key: String, client: &Client) -> Result { + let mut conn = client.get_connection().unwrap(); + let code = redis::cmd("GET") + .arg(key) + .query(&mut conn) + .unwrap_or_else(|_| String::from("")); + Ok(code) +} +pub async fn room_handler( + ws: warp::ws::Ws, + client: &Client, + active_users: Arc>>>>, +) -> Result { + let conn_mutex = Mutex::new(client.get_connection().unwrap()); + + Ok(ws.on_upgrade(|mut socket| async move { + let first_response = socket.next().await.unwrap().unwrap(); + let connect_id = first_response.to_str().unwrap_or(""); + let key = connect_id.to_string(); + let (tx, rx) = socket.split(); + info!("new user connected to room {key}"); + { + let active_users = active_users.clone(); + //let _ = tx.send(Message::text(code)).await; + let mut users_locked = active_users.lock().await; + let room_users = users_locked + .entry(key.clone()) + .or_insert_with(|| Arc::new(Mutex::new(Vec::new()))); + let mut room_users_lock = room_users.lock().await; + room_users_lock.push(tx); + } + info!("added new user connected to room {key}"); + let _ = rx + .try_for_each(|message| { + info!("received message {:?} in room {key}", message); + let active_users = active_users.clone(); + let conn_mutex = &conn_mutex; + let key = &key; + async move { + let mut conn = conn_mutex.lock().await; + if !message.is_text() { + info!("Message is not text"); + return Ok(()); + } + let key = &key; + let active_users = &active_users; + let res = String::from_utf8(message.as_bytes().to_vec()).unwrap(); + let json_res: serde_json::Value = serde_json::from_str(&res).unwrap(); + if let Some(code) = json_res.get("code") { + redis::cmd("SET") + .arg(&(*key).clone()) + .arg(code.as_str()) + .query(&mut *conn) + .unwrap_or_else(|_| String::from("")); + drop(conn); + info!("Received code update"); + } else if let Some(_starts_running) = json_res.get("start_running") { + info!("Room {key} started executing their code"); + } else if let Some(_starts_running) = json_res.get("execution_response") { + info!("Room {key} ended executing their code"); + } + let message = serde_json::to_string(&json_res).unwrap(); + info!("Preparing to send message"); + let mut users_locked = active_users.lock().await; + let user_sockets = users_locked.get_mut(&(*key).clone()).unwrap(); + let mut user_sockets_locked = user_sockets.lock().await; + let sockets_iter = user_sockets_locked.iter_mut(); + info!("Sending message {:?} to all users in room {key}", message); + for socket in sockets_iter { + let _ = socket + .send(Message::text(serde_json::to_string(&message).unwrap())) + .await; + } + info!("Message {:?} sent to all users in room {key}", message); + Ok(()) + } + }) + .await; + })) +} + +pub async fn health_handler(key: String) -> Result { + Ok(key) +} diff --git a/backend/src/run_code.rs b/backend/src/run_code.rs new file mode 100644 index 0000000..6625c6d --- /dev/null +++ b/backend/src/run_code.rs @@ -0,0 +1,55 @@ +use std::process::{Command, Output}; + +use serde_json::json; +use warp::Rejection; + +use crate::Code; +fn stdout_from_out(output: &Output) -> String { + String::from_utf8(output.stdout.clone()).unwrap() +} +fn stderr_from_out(output: &Output) -> String { + String::from_utf8(output.stderr.clone()).unwrap() +} +const TIMED_OUT_STATUS: i32 = 124; +pub async fn code_handler(key: String, body: Code) -> Result { + let _move_program = Command::new("sh") + .arg("-c") + .arg(format!( + "echo \"{}\" | cat > {}.in", + &body.code.replace('\"', r#"\""#), + key + )) + .output() + .unwrap(); + let compile_program = Command::new("sh") + .arg("-c") + .arg(format!("rustc {key}.in")) + .output() + .unwrap(); + let run_program = Command::new("sh") + .arg("-c") + .arg(format!("timeout 10 ./{key}")) + .output() + .unwrap(); + let errors = json!({ + "compile": stderr_from_out(&compile_program), + "run": stderr_from_out(&run_program) + (if run_program.status.code().unwrap_or(0) == TIMED_OUT_STATUS { "\nTimed out" } else { "" }), + }); + let outputs = json!({ + "compile": stdout_from_out(&compile_program), + "run": stdout_from_out(&run_program), + }); + let res = json!({ + "outputs": outputs, + "errors": errors + }); + let _clear = Command::new("sh") + .arg("-c") + .arg(format!("rm {key}.in; rm {key}")) + .output(); + Ok(warp::reply::with_header( + res.to_string(), + "Access-Control-Allow-Origin", + "*", + )) +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..84a5f09 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +dist/ \ No newline at end of file diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml new file mode 100644 index 0000000..195c7b2 --- /dev/null +++ b/frontend/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +yew = "0.19.3" +yew-websocket = "0.1.1" +web-sys = "0.3.60" +reqwest = "0.11.13" +syntect = { version = "5.0", default-features = false, features = ["default-fancy"] } +wasm-bindgen = "0.2.83" +serde_json = "1.0.68" +serde = { version = "1.0.130", features = ["derive"] } +log = "0.4.17" +env_logger = "0.10.0" +yew-router = "0.16" +uuid = { version = "1.2.2", features = ["serde", "v4", "js"] } +gloo-timers = "0.2.6" \ No newline at end of file diff --git a/frontend/Trunk.toml b/frontend/Trunk.toml new file mode 100644 index 0000000..ca93bfc --- /dev/null +++ b/frontend/Trunk.toml @@ -0,0 +1,7 @@ +[[proxy]] +rewrite = "/mysite/api/" +backend = "http://127.0.0.1:8000/" + +[[proxy]] +ws = true +backend = "ws://localhost:8000/ws/room" \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3bab12a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,295 @@ + + + + + + + Document + + + + + + + diff --git a/frontend/src/controlled_textarea.rs b/frontend/src/controlled_textarea.rs new file mode 100644 index 0000000..effb0bd --- /dev/null +++ b/frontend/src/controlled_textarea.rs @@ -0,0 +1,44 @@ +use web_sys::HtmlTextAreaElement; +use yew::prelude::*; +use yew::{function_component, Properties}; +#[derive(Properties, PartialEq)] +pub struct Props { + pub value: String, + pub oninput: yew::Callback, + pub onkeydown: yew::Callback, + pub style: String, + pub cursor: u32, +} +#[function_component(ControlledTextArea)] +pub fn text_area(props: &Props) -> Html { + let ref_ = use_node_ref(); + let ref_2 = ref_.clone(); + + let cursor = props.cursor; + + use_effect(move || { + if let Some(input) = ref_.cast::() { + let _ = input.set_selection_range(cursor, cursor); + } + || {} + }); + let style = props.style.clone(); + let oninput = props.oninput.clone(); + let onkeydown = props.onkeydown.clone(); + + html! { + + } +} diff --git a/frontend/src/home.rs b/frontend/src/home.rs new file mode 100644 index 0000000..6eea129 --- /dev/null +++ b/frontend/src/home.rs @@ -0,0 +1,103 @@ +use std::time::Duration; + +use gloo_timers::callback::Interval; +use web_sys::HtmlInputElement; + +use yew::prelude::*; +use yew_router::prelude::Link; +use yew_router::{prelude::History, scope_ext::RouterScopeExt}; + +use crate::routes::Route; + +#[allow(dead_code)] +pub struct Home { + room_id: String, + carousel_text: Vec, + text_idx: usize, + interval: Interval, +} + +pub enum Msg { + CreateRoom, + RoomIdChanged(String), + NextText, +} + +impl Component for Home { + type Message = Msg; + type Properties = (); + + fn create(ctx: &yew::Context) -> Self { + let carousel_text: Vec = vec![String::from("In a room, you can write Rust code with other people simultaneously and run it remotely. This allows for a more efficient and interactive coding experience."), + String::from("CollabRustAtor allows you to write Rust code, run it, and collaborate with others in real-time. You can either join an existing room by entering the room's ID or create a new room.")]; + let link = ctx.link().clone(); + let interval = Interval::new(Duration::from_secs(10).as_millis() as u32, move || { + link.send_message(Msg::NextText) + }); + Self { + room_id: String::new(), + carousel_text, + text_idx: 0, + interval, + } + } + + fn update(&mut self, ctx: &yew::Context, msg: Self::Message) -> bool { + match msg { + Msg::CreateRoom => { + let new_id = uuid::Uuid::new_v4().to_string(); + ctx.link() + .history() + .unwrap() + .push(Route::Room { id: new_id }); + true + } + Msg::RoomIdChanged(data) => { + self.room_id = data; + true + } + Msg::NextText => { + self.text_idx = (self.text_idx + 1) % self.carousel_text.len(); + true + } + } + } + + fn rendered(&mut self, ctx: &yew::Context, first_render: bool) { + if first_render { + ctx.link().send_message(Msg::NextText); + } + } + + fn view(&self, ctx: &yew::Context) -> Html { + html! { +
+
+
+
+ +
+
+

{"Join a room"}

+
+ + ().value()))}/> + to={Route::Room{id : self.room_id.clone()}} > + + > +
+
+
+
+ Collaborative coding illustration +
+

{self.carousel_text[self.text_idx].clone()}

+
+
+
+
+ } + } +} diff --git a/frontend/src/main.rs b/frontend/src/main.rs new file mode 100644 index 0000000..9dfa9d7 --- /dev/null +++ b/frontend/src/main.rs @@ -0,0 +1,37 @@ +use yew::prelude::*; +use yew_router::prelude::*; +pub mod controlled_textarea; +pub mod home; +pub mod message; +pub mod response; +pub mod room; +pub mod routes; +use crate::home::Home; +use crate::room::Room; +use crate::routes::Route; +#[allow(clippy::let_unit_value)] +fn switch(routes: &Route) -> Html { + match routes { + Route::Home => html! { }, + Route::Room { id } => html! { + + }, + Route::NotFound => html! {

{ "404" }

}, + } +} + +#[function_component(Main)] +fn app() -> Html { + html! { + +
+ to={Route::Home}>

{"CollabRustAtor"}

> +
+ render={Switch::render(switch)} /> +
+ } +} + +fn main() { + yew::start_app::
(); +} diff --git a/frontend/src/message.rs b/frontend/src/message.rs new file mode 100644 index 0000000..f75e6ca --- /dev/null +++ b/frontend/src/message.rs @@ -0,0 +1,11 @@ +use crate::response::Res; + +pub enum Msg { + InputChange(String, u32), + SetContent(String), + SendCode, + SetResponse(Res), + SetResponseNoWs(Res), + Empty, + SendMyId, +} diff --git a/frontend/src/response.rs b/frontend/src/response.rs new file mode 100644 index 0000000..bb983c8 --- /dev/null +++ b/frontend/src/response.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default)] +#[serde()] +pub struct SpecificResponse { + pub compile: String, + pub run: String, +} +#[derive(Serialize, Deserialize, Default)] +#[serde()] + +pub struct Res { + pub errors: SpecificResponse, + pub outputs: SpecificResponse, +} + +impl Res { + pub fn to_message(&self) -> String { + let mut output = String::from("Result:\n"); + if !self.outputs.run.is_empty() { + output = format!("{}{}", output, self.outputs.run); + } + if !self.errors.compile.is_empty() { + return format!("Compilation message:\n{}\n{output}", self.errors.compile); + } + if !self.errors.run.is_empty() { + return format!( + "Partial result:\n{}\nRuntime error:\n{}", + self.outputs.run, self.errors.run + ); + } + output + } +} diff --git a/frontend/src/room.rs b/frontend/src/room.rs new file mode 100644 index 0000000..785fdf5 --- /dev/null +++ b/frontend/src/room.rs @@ -0,0 +1,257 @@ +extern crate log; +use crate::controlled_textarea::ControlledTextArea; +use crate::message::Msg; +use crate::response::{Res, SpecificResponse}; +use serde_json::json; +use syntect::highlighting::{Theme, ThemeSet}; +use syntect::html::highlighted_html_for_string; +use syntect::parsing::{SyntaxReference, SyntaxSet}; +use wasm_bindgen::JsCast; +use web_sys::{window, HtmlTextAreaElement}; +use yew::prelude::*; +use yew_websocket::macros::Json; +use yew_websocket::websocket::{WebSocketService, WebSocketStatus, WebSocketTask}; +pub struct Room { + code: String, + ss: SyntaxSet, + theme: Theme, + syntax: SyntaxReference, + html: String, + code_response: Res, + ws: WebSocketTask, + cursor: u32, +} +#[derive(Properties, PartialEq, Eq)] +pub struct Props { + pub id: String, +} +const TAB_KEYCODE: u32 = 9; +impl Component for Room { + type Message = Msg; + type Properties = Props; + + fn create(ctx: &Context) -> Self { + let callback = ctx.link().callback(|Json(data): Json>| { + let data = data.unwrap(); + let js: serde_json::Value = serde_json::from_str(&data).unwrap(); + if let Some(code) = js.get("code") { + return Msg::SetContent(code.as_str().unwrap().to_string()); + } + if let Some(_code) = js.get("start_running") { + return Msg::Empty; + } + if let Some(response) = js.get("execution_response") { + let res: Res = serde_json::from_value(response.clone()).unwrap(); + return Msg::SetResponseNoWs(res); + } + Msg::Empty + }); + let status_callback = ctx.link().callback(|status| { + if status == WebSocketStatus::Opened { + return Msg::SendMyId; + } + Msg::Empty + }); + let baseurl = web_sys::window().unwrap().origin().replace("http", "ws"); + let ws = WebSocketService::connect_text( + format!("{baseurl}/ws/room").as_str(), + callback, + status_callback, + ) + .unwrap(); + let code = "\n".to_string(); + let ss = SyntaxSet::load_defaults_newlines(); + let ts = ThemeSet::load_defaults(); + let theme = ts.themes["base16-ocean.dark"].clone(); + let syntax = ss.find_syntax_by_extension("rs").unwrap().to_owned(); + let html = highlighted_html_for_string(&code, &ss, &syntax, &theme).expect("Can't parse"); + let id = ctx.props().id.clone(); + let baseurl = web_sys::window().unwrap().origin(); + ctx.link().send_future(async move { + let client = reqwest::Client::new(); + let res = client + .get(format!("{baseurl}/mysite/api/room/{}/code", id)) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + Msg::SetContent(res) + }); + Self { + code, + ss, + theme, + syntax, + html, + ws, + code_response: Res { + ..Default::default() + }, + cursor: 0, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::InputChange(content, cursor) => { + let message = json!({ "code": content }); + self.ws.send(Ok(serde_json::to_string(&message).unwrap())); + ctx.link().send_message(Msg::SetContent(content)); + self.cursor = cursor; + false + } + Msg::Empty => false, + Msg::SetContent(content) => { + self.code = content; + let code_with_endline = self.code.clone() + "\n"; + self.html = highlighted_html_for_string( + &code_with_endline, + &self.ss, + &self.syntax, + &self.theme, + ) + .expect("Can't parse"); + true + } + Msg::SendCode => { + let message = json!({ + "start_running": true + }); + self.ws.send(Ok(serde_json::to_string(&message).unwrap())); + let code = self.code.clone(); + ctx.link().send_message(Msg::SetResponse(Res { + errors: SpecificResponse { + compile: String::from(""), + run: String::from(""), + }, + outputs: SpecificResponse { + compile: String::from(""), + run: String::from("Compiling/Running..."), + }, + })); + let id = ctx.props().id.clone(); + ctx.link().send_future(async move { + let client = reqwest::Client::new(); + let baseurl = web_sys::window().unwrap().origin(); + let res = client + .post(format!("{baseurl}/mysite/api/code/{id}")) + .header("Content-Type", "application/json") + .body(format!( + "{{\"code\": \"{}\"}}", + code.clone().replace('\"', "\\\"").replace('\n', "") + )) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + let res_json: Res = + serde_json::from_str(&res).expect("Failed to parse response"); + Msg::SetResponse(res_json) + }); + true + } + Msg::SetResponse(res) => { + let message = json!({ "execution_response": res }); + self.ws.send(Ok(serde_json::to_string(&message).unwrap())); + self.code_response = res; + true + } + Msg::SetResponseNoWs(res) => { + self.code_response = res; + true + } + Msg::SendMyId => { + let id = ctx.props().id.clone(); + self.ws.send(Ok(id)); + false + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let html = self.html.clone(); + let (new_area_height, new_area_width) = match window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("editor")) + { + None => ("2rem".to_string(), "100%".to_string()), + Some(e) => { + e.set_inner_html(&html); + ( + format!("{}px", e.client_height()), + format!("{}px" /* subtract left margin */, e.client_width()), + ) + } + }; + let rows = &html.lines().count() - 2; + let mut arr = vec![]; + let mut i = 0; + while i < rows { + arr.push(i + 1); + i += 1; + } + let on_textarea_keydown = |e: KeyboardEvent| { + let text_area = e.target().unwrap().unchecked_into::(); + if e.key_code() == TAB_KEYCODE { + let spaces_in_tab: u32 = 4; + e.prevent_default(); + let start = text_area.selection_start().unwrap_or(None).unwrap_or(0); + let end = text_area.selection_end().unwrap_or(None).unwrap_or(0); + let current_text = text_area.value(); + text_area.set_value( + format!( + "{}{}{}", + ¤t_text + .chars() + .into_iter() + .take(start as usize) + .collect::() + .as_str(), + " ".repeat(spaces_in_tab as usize), + ¤t_text + .chars() + .into_iter() + .skip(end as usize) + .collect::() + .as_str() + ) + .as_str(), + ); + text_area + .set_selection_range(start + spaces_in_tab, end + spaces_in_tab) + .unwrap_or_default(); + let cursor = text_area.selection_start().unwrap_or(None).unwrap_or(0); + return Msg::InputChange(text_area.value(), cursor); + } + Msg::Empty + }; + log::info!("Render"); + let cursor = self.cursor; + html! { +
+ /*