From 1714b4fd7d18d1baf05d2d025c6da5a76387d943 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Fri, 11 Nov 2022 08:45:36 +0000 Subject: [PATCH 01/17] Setting up GitHub Classroom Feedback From 99680847f3b0d399afc2647358e49cd50ad49b1f Mon Sep 17 00:00:00 2001 From: Jan Wojtach Date: Fri, 11 Nov 2022 10:09:45 +0100 Subject: [PATCH 02/17] Add project description --- README.md | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 58e9e49..7abe10d 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,35 @@ -# 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 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). + +In the third part I'm going to implement 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) + +## Additional tech stack + +- Docker +- Database (ie. MySQL) From 7b12e4795635154a2c706c0beb91a4adb39073c2 Mon Sep 17 00:00:00 2001 From: Jan Wojtach Date: Sun, 13 Nov 2022 12:27:22 +0100 Subject: [PATCH 03/17] Merge plan parts --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7abe10d..8c14842 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ CollabRustAtor is going to be a live collaborative rust code editor and compiler 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 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). +In the second part I'm going to add: -In the third part I'm going to implement concurrent editing- every client will be connected to WebSocket server and send/receive pieces of information about new file changes in certain room. +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 From 314c451da3bd6255e18a877e9b5f28c9e1413747 Mon Sep 17 00:00:00 2001 From: Jan Wojtach Date: Sat, 17 Dec 2022 17:56:22 +0100 Subject: [PATCH 04/17] Set up project scaffolding --- backend/.gitignore | 14 +++ backend/Cargo.toml | 14 +++ backend/src/main.rs | 59 ++++++++++++ backend/src/room.rs | 4 + backend/src/run_code.rs | 58 ++++++++++++ frontend/.gitignore | 16 ++++ frontend/Cargo.toml | 15 +++ frontend/index.html | 102 ++++++++++++++++++++ frontend/src/main.rs | 196 +++++++++++++++++++++++++++++++++++++++ frontend/src/message.rs | 7 ++ frontend/src/response.rs | 33 +++++++ 11 files changed, 518 insertions(+) create mode 100644 backend/.gitignore create mode 100644 backend/Cargo.toml create mode 100644 backend/src/main.rs create mode 100644 backend/src/room.rs create mode 100644 backend/src/run_code.rs create mode 100644 frontend/.gitignore create mode 100644 frontend/Cargo.toml create mode 100644 frontend/index.html create mode 100644 frontend/src/main.rs create mode 100644 frontend/src/message.rs create mode 100644 frontend/src/response.rs diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ada8be9 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,14 @@ +# 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 \ No newline at end of file diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..dd56f7c --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,14 @@ +[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 = "0.2", features = ["macros", "sync"] } +warp = "0.2" +serde = {version = "1.0", features = ["derive"] } +serde_json = "1.0" +futures = { version = "0.3", default-features = false } +uuid = { version = "0.4", features = ["serde", "v4"] } \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..48c9d32 --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,59 @@ +use room::room_handler; +use crate::run_code::code_handler; +use serde::{Deserialize, Serialize}; +use warp::{self, hyper::Method, 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))) +} + +#[tokio::main] +async fn main() { + 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 room_route = warp::path!("room") + .and(warp::path::param()) + .and_then(room_handler); + + let code_route = warp::path("code") + .and(warp::post()) + .and(warp::body::json()) + .and_then(code_handler); + + let routes = room_route + .or(code_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..f85c884 --- /dev/null +++ b/backend/src/room.rs @@ -0,0 +1,4 @@ +use warp::{Rejection}; +pub async fn room_handler(id: String) -> Result { + Ok(id) +} \ No newline at end of file diff --git a/backend/src/run_code.rs b/backend/src/run_code.rs new file mode 100644 index 0000000..eaf82fc --- /dev/null +++ b/backend/src/run_code.rs @@ -0,0 +1,58 @@ +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() +} + +pub async fn code_handler(body: Code) -> Result { + let s = format!( + "docker run -v $(pwd):/home --rm rust sh -c 'echo \"{}\" | cat > /home/a.in; rustc /home/a.in > /home/compile.txt 2> /home/compile_err.txt; timeout 10 ./a > /home/run.txt 2> /home/run_err.txt; status=$?; if [ $status -eq 127 ]; then echo \"\" | cat > /home/run_err.txt; fi; if [ $status -eq 124 ]; then echo \"Timed out\" | cat > /home/run_err.txt; fi;'", + &body.code.replace('\"', "\\\"") + ); + let _out = Command::new("sh") + .arg("-c") + .arg(s) + .output() + .expect("failed"); + let out_compile = Command::new("sh") + .arg("-c") + .arg("cat compile.txt") + .output() + .expect("failed"); + let err_compile = Command::new("sh") + .arg("-c") + .arg("cat compile_err.txt") + .output() + .expect("failed"); + let out_run = Command::new("sh") + .arg("-c") + .arg("cat run.txt") + .output() + .expect("failed"); + let err_run = Command::new("sh") + .arg("-c") + .arg("cat run_err.txt") + .output() + .expect("failed"); + let errors = json!({ + "compile": stdout_from_out(&err_compile), + "run": stdout_from_out(&err_run), + }); + let outputs = json!({ + "compile": stdout_from_out(&out_compile), + "run": stdout_from_out(&out_run), + }); + let res = json!({ + "outputs": outputs, + "errors": errors + }); + 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..00f5f6d --- /dev/null +++ b/frontend/Cargo.toml @@ -0,0 +1,15 @@ +[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" +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"] } diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..4887a87 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,102 @@ + + + + + + + Document + + + + diff --git a/frontend/src/main.rs b/frontend/src/main.rs new file mode 100644 index 0000000..d6ad4ff --- /dev/null +++ b/frontend/src/main.rs @@ -0,0 +1,196 @@ +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, Element, HtmlTextAreaElement}; +use yew::prelude::*; +pub mod message; +pub mod response; +use crate::message::Msg; +use crate::response::{Res, SpecificResponse}; +pub struct Main { + code: String, + ss: SyntaxSet, + theme: Theme, + syntax: SyntaxReference, + html: String, + div: Element, + code_response: Res, +} +impl Component for Main { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + let code = "\n".repeat(20); + 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 div = web_sys::window() + .unwrap() + .document() + .unwrap() + .create_element("div") + .unwrap(); + div.set_inner_html(&html); + Self { + code, + ss, + theme, + syntax, + html, + div, + code_response: Res { + errors: SpecificResponse { + compile: String::from(""), + run: String::from(""), + }, + outputs: SpecificResponse { + compile: String::from(""), + run: String::from(""), + }, + }, + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::SetContent(content) => { + self.code = content + "\n"; + self.html = + highlighted_html_for_string(&self.code, &self.ss, &self.syntax, &self.theme) + .expect("Can't parse"); + self.div.set_inner_html(&self.html); + true + } + Msg::SendCode => { + 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..."), + }, + })); + _ctx.link().send_future(async move { + let client = reqwest::Client::new(); + let res = client + .post("http://127.0.0.1:8000/code") + .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) => { + self.code_response = res; + true + } + } + } + + fn rendered(&mut self, _: &Context, first_render: bool) { + if first_render { + window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("editor")) + .unwrap() + .set_inner_html(&self.html.clone()); + window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("area")) + .unwrap() + .set_inner_html(&self.code.clone()); + } + } + + fn view(&self, ctx: &Context) -> Html { + let html = self.html.clone(); + let new_area_height = match window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("editor")) + { + None => "30rem".to_string(), + Some(e) => { + e.set_inner_html(&html); + format!("{}px", e.client_height()) + } + }; + 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() == 9 { + 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{}", + ¤t_text + .chars() + .into_iter() + .take(start as usize) + .collect::() + .as_str(), + ¤t_text + .chars() + .into_iter() + .skip(end as usize) + .collect::() + .as_str() + ) + .as_str(), + ); + text_area + .set_selection_range(start + 1, end + 1) + .unwrap_or_default(); + //let _r = text_area.set_range_text_with_start_and_end("\t", start, end); + } + Msg::SetContent(text_area.value()) + }; + html! { +
+ + } +} diff --git a/frontend/src/main.rs b/frontend/src/main.rs index d7d1a1f..9dfa9d7 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -1,5 +1,6 @@ use yew::prelude::*; use yew_router::prelude::*; +pub mod controlled_textarea; pub mod home; pub mod message; pub mod response; diff --git a/frontend/src/message.rs b/frontend/src/message.rs index 7120c76..f75e6ca 100644 --- a/frontend/src/message.rs +++ b/frontend/src/message.rs @@ -1,7 +1,7 @@ use crate::response::Res; pub enum Msg { - InputChange(String), + InputChange(String, u32), SetContent(String), SendCode, SetResponse(Res), diff --git a/frontend/src/room.rs b/frontend/src/room.rs index d5deb20..785fdf5 100644 --- a/frontend/src/room.rs +++ b/frontend/src/room.rs @@ -1,4 +1,5 @@ extern crate log; +use crate::controlled_textarea::ControlledTextArea; use crate::message::Msg; use crate::response::{Res, SpecificResponse}; use serde_json::json; @@ -18,6 +19,7 @@ pub struct Room { html: String, code_response: Res, ws: WebSocketTask, + cursor: u32, } #[derive(Properties, PartialEq, Eq)] pub struct Props { @@ -87,15 +89,17 @@ impl Component for Room { code_response: Res { ..Default::default() }, + cursor: 0, } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - Msg::InputChange(content) => { + 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, @@ -177,7 +181,10 @@ impl Component for Room { 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())) + ( + format!("{}px", e.client_height()), + format!("{}px" /* subtract left margin */, e.client_width()), + ) } }; let rows = &html.lines().count() - 2; @@ -217,14 +224,21 @@ impl Component for Room { text_area .set_selection_range(start + spaces_in_tab, end + spaces_in_tab) .unwrap_or_default(); - return Msg::InputChange(text_area.value()); + 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! {
-