From a1ed12f4f4e11420f5403dffee0a97fb09c2a43f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 May 2019 21:51:17 -0700 Subject: [PATCH] Start work on web UI command --- Cargo.lock | Bin 37654 -> 43034 bytes cli/Cargo.toml | 2 + cli/src/lib.rs | 1 + cli/src/main.rs | 5 +- cli/src/wasm.rs | 15 +++-- cli/src/web_ui.html | 111 +++++++++++++++++++++++++++++++++++ cli/src/web_ui.rs | 60 +++++++++++++++++++ docs/assets/js/playground.js | 19 +++--- 8 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 cli/src/web_ui.html create mode 100644 cli/src/web_ui.rs diff --git a/Cargo.lock b/Cargo.lock index a28a6889dc5240302febae5016b3b19ffc1f3a56..6248034bed7caa37d1f51fa0c4b469c2f89615a7 100644 GIT binary patch delta 1889 zcmaJ>%Zr;;6lbQHSJKvEA44nFFuHJ%hWmb4rs*JU5liVqrw=UQcRV8nVM*H+O2kee5jG{A1>wFFWuW&o$EDM zph{}ACi2GR3)RqAZQX!)wbya2Zi3E8GM?$F6ZwaU{l(+T`O1#ia=CJ-QCV@l(130~ zPyT(TzGt=74DAR<_Eu?fm?}T0 zzB8!v*$dM%hoKXo)jI+$%-U>yxD6DoWpe7|;OrN#+@3#x{cQU_=#Ax{k1gdFUw$(` zeCuqklMfE|nyrtnHhR6ABZpO9oIP>((cX&dbBXXwO=o)c*vx=Dd>46rvc7MWr$LwN z^Yy%T{^ZnZXSqu8+WpIK7qv4#PVIbF3r{JGF66H+9?0dT>h9{?8hGDnuXUkQC3)la zLO#3uV6L616sMOa#t-xf90AA?0X8$j`GCOu!=>q6+vr6a8~JZ+8|m$TEzVtfF!p?B zd0Wmt)M3-5W?#`Krx$ls=SDQ$cXLDp?2zR3`Jr;9D-XwJHap1|?Dgpg-ChU9${W3U zuI?}OSud|OH^bk)e=&dl+}`4+cfTo>w{qP&R@WYkr7i@agJV<(Wq?@0WF#y=;x4gZ zMI_NMVral9Q8Zbr3}K0iqJ8bXi7kRH-mPB~(o$v&rHls8RB}-VZUWWb>F69aS_!SG z;mUK7oY#>jA1#NdI5*nNmFLjn;YN3L+PS2dV$xfmm;%Lh zNLq6xs16ZmB*EH*e1SF{FZ1LP?AaP)g3D^g=m&P%f-w z+#16SuUXofpqPj6X&gRCHIVwff$th}u%1Pu_I42UfHfmvfx=4KU@nqjihgEgH error::Result<()> { .about("Compile a parser to WASM") .arg(Arg::with_name("path").index(1).multiple(true)), ) + .subcommand(SubCommand::with_name("ui").about("Test a parser interactively in the browser")) .get_matches(); let home_dir = dirs::home_dir().expect("Failed to read home directory"); @@ -245,6 +246,8 @@ fn run() -> error::Result<()> { } else if let Some(matches) = matches.subcommand_matches("build-wasm") { let grammar_path = current_dir.join(matches.value_of("path").unwrap_or("")); wasm::compile_language_to_wasm(&grammar_path)?; + } else if matches.subcommand_matches("ui").is_some() { + web_ui::serve(¤t_dir); } Ok(()) diff --git a/cli/src/wasm.rs b/cli/src/wasm.rs index 782b9a439..3fa38c65f 100644 --- a/cli/src/wasm.rs +++ b/cli/src/wasm.rs @@ -5,10 +5,7 @@ use std::fs; use std::path::Path; use std::process::Command; -pub fn compile_language_to_wasm(language_dir: &Path) -> Result<()> { - let src_dir = language_dir.join("src"); - - // Parse the grammar.json to find out the language name. +pub fn get_grammar_name(src_dir: &Path) -> Result { let grammar_json_path = src_dir.join("grammar.json"); let grammar_json = fs::read_to_string(&grammar_json_path).map_err(|e| { format!( @@ -22,7 +19,13 @@ pub fn compile_language_to_wasm(language_dir: &Path) -> Result<()> { grammar_json_path, e ) })?; - let output_filename = format!("tree-sitter-{}.wasm", grammar.name); + Ok(grammar.name) +} + +pub fn compile_language_to_wasm(language_dir: &Path) -> Result<()> { + let src_dir = language_dir.join("src"); + let grammar_name = get_grammar_name(&src_dir)?; + let output_filename = format!("tree-sitter-{}.wasm", grammar_name); // Get the current user id so that files created in the docker container will have // the same owner. @@ -54,7 +57,7 @@ pub fn compile_language_to_wasm(language_dir: &Path) -> Result<()> { "-s", "TOTAL_MEMORY=33554432", "-s", - &format!("EXPORTED_FUNCTIONS=[\"_tree_sitter_{}\"]", grammar.name), + &format!("EXPORTED_FUNCTIONS=[\"_tree_sitter_{}\"]", grammar_name), "-fno-exceptions", "-I", "src", diff --git a/cli/src/web_ui.html b/cli/src/web_ui.html new file mode 100644 index 000000000..58ab0e7f9 --- /dev/null +++ b/cli/src/web_ui.html @@ -0,0 +1,111 @@ + + Tree-sitter + + + + + +
+
+
+ + +
+ +
+ + +
+
+ +
+ + + + +
+

+      
+
+
+ + + + + + + + + + + + diff --git a/cli/src/web_ui.rs b/cli/src/web_ui.rs new file mode 100644 index 000000000..2ea0b4ab0 --- /dev/null +++ b/cli/src/web_ui.rs @@ -0,0 +1,60 @@ +use super::wasm; +use std::fs; +use std::net::TcpListener; +use std::path::Path; +use std::str::FromStr; +use tiny_http::{Header, Response, Server}; +use webbrowser; + +const PLAYGROUND_JS: &'static [u8] = include_bytes!("../../docs/assets/js/playground.js"); +const LIB_JS: &'static [u8] = include_bytes!("../../lib/binding_web/tree-sitter.js"); +const LIB_WASM: &'static [u8] = include_bytes!("../../lib/binding_web/tree-sitter.wasm"); +const HTML: &'static [u8] = include_bytes!("./web_ui.html"); + +pub fn serve(grammar_path: &Path) { + let port = get_available_port().expect("Couldn't find an available port"); + let url = format!("127.0.0.1:{}", port); + let server = Server::http(&url).expect("Failed to start web server"); + let grammar_name = wasm::get_grammar_name(&grammar_path.join("src")) + .map_err(|e| format!("Failed to get wasm filename: {:?}", e)) + .unwrap(); + let language_wasm = fs::read(format!("./tree-sitter-{}.wasm", grammar_name)).unwrap(); + + webbrowser::open(&format!("http://127.0.0.1:{}", port)) + .map_err(|e| format!("Failed to open '{}' in a web browser. Error: {}", url, e)) + .unwrap(); + + let html_header = Header::from_str("Content-type: text/html").unwrap(); + let js_header = Header::from_str("Content-type: application/javascript").unwrap(); + let wasm_header = Header::from_str("Content-type: application/wasm").unwrap(); + + for request in server.incoming_requests() { + let (body, header) = match request.url() { + "/" => (HTML, &html_header), + "/playground.js" => (PLAYGROUND_JS, &js_header), + "/tree-sitter.js" => (LIB_JS, &js_header), + "/tree-sitter.wasm" => (LIB_WASM, &wasm_header), + "/tree-sitter-parser.wasm" => (language_wasm.as_slice(), &wasm_header), + _ => { + request + .respond(Response::from_string("Not found").with_status_code(404)) + .expect("Failed to write HTTP response"); + continue; + } + }; + let response = Response::from_string("") + .with_data(body, Some(body.len())) + .with_header(header.clone()); + request + .respond(response) + .expect("Failed to write HTTP response"); + } +} + +fn get_available_port() -> Option { + (8000..12000).find(port_is_available) +} + +fn port_is_available(port: &u16) -> bool { + TcpListener::bind(("127.0.0.1", *port)).is_ok() +} diff --git a/docs/assets/js/playground.js b/docs/assets/js/playground.js index 5f8ff923d..6801d6995 100644 --- a/docs/assets/js/playground.js +++ b/docs/assets/js/playground.js @@ -11,10 +11,7 @@ let tree; const demoContainer = document.getElementById('playground-container'); const languagesByName = {}; - await Promise.all([ - codeInput.value = await fetch(scriptURL).then(r => r.text()), - TreeSitter.init() - ]); + await TreeSitter.init(); const parser = new TreeSitter(); const codeEditor = CodeMirror.fromTextArea(codeInput, { @@ -182,11 +179,13 @@ let tree; const node = tree.rootNode.namedDescendantForPosition(start, end); if (treeRows) { if (treeRowHighlightedIndex !== -1) { - treeRows[treeRowHighlightedIndex] = treeRows[treeRowHighlightedIndex].replace('highlighted', 'plain'); + const row = treeRows[treeRowHighlightedIndex]; + if (row) treeRows[treeRowHighlightedIndex] = row.replace('highlighted', 'plain'); } treeRowHighlightedIndex = treeRows.findIndex(row => row.includes(`data-id=${node.id}`)); if (treeRowHighlightedIndex !== -1) { - treeRows[treeRowHighlightedIndex] = treeRows[treeRowHighlightedIndex].replace('plain', 'highlighted'); + const row = treeRows[treeRowHighlightedIndex]; + if (row) treeRows[treeRowHighlightedIndex] = row.replace('plain', 'highlighted'); } cluster.update(treeRows); const lineHeight = cluster.options.item_height; @@ -220,7 +219,13 @@ let tree; function handleLoggingChange() { if (loggingCheckbox.checked) { - parser.setLogger(console.log); + parser.setLogger((message, lexing) => { + if (lexing) { + console.log(" ", message) + } else { + console.log(message) + } + }); } else { parser.setLogger(null); }