Start work on web UI command

This commit is contained in:
Max Brunsfeld 2019-05-13 21:51:17 -07:00
parent 7bc7306afb
commit a1ed12f4f4
8 changed files with 316 additions and 14 deletions

View file

@ -9,6 +9,7 @@ pub mod properties;
pub mod test;
pub mod util;
pub mod wasm;
pub mod web_ui;
#[cfg(test)]
mod tests;

View file

@ -5,7 +5,7 @@ use std::path::Path;
use std::process::exit;
use std::{u64, usize};
use tree_sitter_cli::{
config, error, generate, highlight, loader, logger, parse, properties, test, wasm,
config, error, generate, highlight, loader, logger, parse, properties, test, wasm, web_ui,
};
fn main() {
@ -95,6 +95,7 @@ fn run() -> 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(&current_dir);
}
Ok(())

View file

@ -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<String> {
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",

111
cli/src/web_ui.html Normal file
View file

@ -0,0 +1,111 @@
<head>
<title>Tree-sitter</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.css">
</head>
<body>
<div id="playground-container">
<header>
<div class=header-item>
<label for="logging-checkbox">Log</label>
<input id="logging-checkbox" type="checkbox"></input>
</div>
<div class=header-item>
<label for="update-time">Update time: </label>
<span id="update-time"></span>
</div>
</header>
<main>
<select id="language-select" style="display: none;">
<option value="parser">Parser</option>
</select>
<textarea id="code-input"></textarea>
<div id="output-container-scroll">
<pre id="output-container" class="highlight"></pre>
</div>
</main>
</div>
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
crossorigin="anonymous">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.js"></script>
<script>LANGUAGE_BASE_URL = "";</script>
<script src=tree-sitter.js></script>
<script src=playground.js></script>
<style>
#playground-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
}
header {
box-sizing: border-box;
display: flex;
padding: 20px;
height: 60px;
border-bottom: 1px solid #aaa;
}
main {
display: flex;
height: 100%;
flex-direction: row;
}
.header-item {
margin-right: 20px;
}
.CodeMirror {
width: 50%;
height: 100%;
border-right: 1px solid #aaa;
}
#output-container-scroll {
width: 50%;
height: 100%;
padding: 0;
overflow: auto;
}
#output-container {
padding: 0 10px;
margin: 0;
}
h4, select, .field {
display: inline-block;
margin-right: 20px;
}
#logging-checkbox {
height: 15px;
}
.CodeMirror div.CodeMirror-cursor {
border-left: 3px solid red;
}
a.highlighted {
background-color: #ddd;
text-decoration: underline;
}
</style>
</body>

60
cli/src/web_ui.rs Normal file
View file

@ -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<u16> {
(8000..12000).find(port_is_available)
}
fn port_is_available(port: &u16) -> bool {
TcpListener::bind(("127.0.0.1", *port)).is_ok()
}