Original commit: b16ac40a3d
This commit is contained in:
Wojciech Daniło 2020-12-24 05:38:01 +01:00 committed by GitHub
parent 6a7bee1776
commit 89d990c5b3
12 changed files with 269 additions and 161 deletions

View File

@ -72,17 +72,18 @@ function command(docs) {
return {docs}
}
function run_project_manager() {
const bin_path = paths.get_project_manager_path(paths.dist.bin)
console.log(`Starting the language server from "${bin_path}".`)
child_process.execFile(bin_path, [], (error, stdout, stderr) => {
console.error(stderr)
if (error) {
throw error
}
console.log(stdout)
})
}
// FIXME: this does not work if project manager was not downloaded yet.
//function run_project_manager() {
// const bin_path = paths.get_project_manager_path(paths.dist.bin)
// console.log(`Starting the language server from "${bin_path}".`)
// child_process.execFile(bin_path, [], (error, stdout, stderr) => {
// console.error(stderr)
// if (error) {
// throw error
// }
// console.log(stdout)
// })
//}
// ================
// === Commands ===
@ -241,7 +242,8 @@ commands.watch.rust = async function(argv) {
build_args.push(`--crate=${argv.crate}`)
}
run_project_manager()
// FIXME: See fixme on fn definition.
// run_project_manager()
build_args = build_args.join(' ')
let target =
'"' +

View File

@ -147,9 +147,9 @@ are presented below:
utility which will build the project on every change. Open
`http://localhost:8080` (the port may vary and will be reported in the
terminal if `8080` was already in use) to run the application, or
`http://localhost:8080/debug` to open example demo scenes. Please remember
to disable the cache in your browser during the development! By default,
the script disables heavyweight optimizations to provide interactive
`http://localhost:8080/?entry` to open example demo scenes list. Please
remember to disable the cache in your browser during the development! By
default, the script disables heavyweight optimizations to provide interactive
development experience. The scripts are thin wrappers for
[wasm-pack](https://github.com/rustwasm/wasm-pack) and accept the same
[command line arguments](https://rustwasm.github.io/wasm-pack/book/commands/build.html).
@ -158,8 +158,8 @@ are presented below:
In order to compile in a production mode (enable all optimizations, strip
WASM debug symbols, minimize the output binaries, etc.), run
`node ./run build`. To create platform-specific packages and installers use
`node ./run dist` instead. The final packages will be located at
`app/dist/native`.
`node ./run dist` instead. The final executables will be located at
`dist/client/$PLATFORM`.
- **Selective mode**
In order to compile only part of the project, and thus drastically shorten
@ -173,6 +173,19 @@ are presented below:
were defined or re-exported by that crate. In particular, the `ide` crate
exposes the `entry_point_ide` function, so you have to compile it to test
your code in the Enso IDE.
### Using IDE as a library.
In case you want to use the IDE as a library, for example to embed it into
another website, you need to first build it using `node ./run {built,dist}` and
find the necessary artifacts located at `dist/content`. Especially, the
`dist/content/index.js` defines a function `window.enso.main(cfg)` which you
can use to run the IDE. Currently, the configuration argument can contain the
following options:
- `entry` - the entry point, one of predefined scenes. Set it to empty string to
see the list of possible entry points.
- `project` - the project name to open after loading the IDE.
### Testing, Linting, and Validation
After changing the code it's always a good idea to lint and test the code. We

8
gui/src/config.yaml Normal file
View File

@ -0,0 +1,8 @@
# The name of an object created in the global window scope. This object will contain functions
# to control the IDE, like the `main` function, and also the configuration object.
windowAppScopeName: "enso"
# The configuration object nested inside of the `windowAppScopeName` object, containing all
# startup configuration options. See usages of this variable to learn more about available
# options.
windowAppScopeConfigName: "config"

View File

@ -10,7 +10,8 @@ let config = {
},
devDependencies: {
"compression-webpack-plugin": "^3.1.0",
"copy-webpack-plugin": "^5.1.1"
"copy-webpack-plugin": "^5.1.1",
"yaml-loader": "^0.6.0",
}
}

View File

@ -69,9 +69,9 @@
}
</style>
<script type="module" src="/assets/index.js" defer></script>
<script type="module" src="/assets/run.js" defer></script>
</head>
<body>
<!-- <div class="titlebar"></div> -->
<div id="root"></div>
<noscript>
This page requires JavaScript to run. Please enable it in your browser.

View File

@ -5,6 +5,16 @@
import * as loader_module from 'enso-studio-common/src/loader'
import * as html_utils from 'enso-studio-common/src/html_utils'
import * as animation from 'enso-studio-common/src/animation'
import * as globalConfig from '../../../../config.yaml'
// ==================
// === Global API ===
// ==================
let API = {}
window[globalConfig.windowAppScopeName] = API
@ -109,18 +119,13 @@ function show_debug_screen(wasm,msg) {
ul.appendChild(li)
a.appendChild(linkText)
a.title = name
a.href = "javascript:{}"
a.onclick = () => {
html_utils.remove_node(debug_screen_div)
let fn_name = wasm_entry_point_pfx + name
let fn = wasm[fn_name]
fn()
}
a.href = "?entry="+name
li.appendChild(a)
}
}
// ====================
// === Scam Warning ===
// ====================
@ -191,7 +196,7 @@ window.logsBuffer = logsBuffer
let root = document.getElementById('root')
function prepare_root(cfg) {
function style_root() {
root.style.backgroundColor = '#f6f3f199'
}
@ -218,43 +223,36 @@ function disableContextMenu() {
})
}
function ok(value) {
return value !== null && value !== undefined
}
/// Main entry point. Loads WASM, initializes it, chooses the scene to run.
async function main() {
API.main = async function (inputConfig) {
let urlParams = new URLSearchParams(window.location.search);
let urlConfig = Object.fromEntries(urlParams.entries())
let config = Object.assign({},inputConfig,urlConfig)
API[globalConfig.windowAppScopeConfigName] = config
style_root()
printScamWarning()
hideLogs()
disableContextMenu()
let location = window.location.pathname.split('/')
location.splice(0,1)
let cfg = getUrlParams()
prepare_root(cfg)
let debug_mode = location[0] == "debug"
let debug_target = location[1]
let no_loader = debug_mode && debug_target
let entryTarget = ok(config.entry) ? config.entry : main_entry_point
let useLoader = entryTarget === main_entry_point
await windowShowAnimation()
let {wasm,loader} = await download_content({no_loader})
let {wasm,loader} = await download_content({no_loader:!useLoader})
let target = null;
if (debug_mode) {
loader.destroy()
if (debug_target) {
target = debug_target
}
} else {
target = main_entry_point
}
if (target) {
let fn_name = wasm_entry_point_pfx + target
if (entryTarget) {
let fn_name = wasm_entry_point_pfx + entryTarget
let fn = wasm[fn_name]
if (fn) { fn() } else {
loader.destroy()
show_debug_screen(wasm,"Unknown entry point '" + target + "'. ")
show_debug_screen(wasm,"Unknown entry point '" + entryTarget + "'. ")
}
} else {
show_debug_screen(wasm)
}
}
main()

View File

@ -0,0 +1,4 @@
/// This file is used to simply run the IDE. It can be not invoked if the IDE needs to be used as a
/// library.
window.enso.main()

View File

@ -24,6 +24,7 @@ module.exports = {
new CompressionPlugin(),
new CopyWebpackPlugin([
path.resolve(thisPath,'src','index.html'),
path.resolve(thisPath,'src','run.js'),
path.resolve(wasmPath,'ide.wasm'),
]),
],
@ -42,5 +43,14 @@ module.exports = {
hints: false,
},
mode: 'none',
stats: 'minimal'
stats: 'minimal',
module: {
rules: [
{
test: /\.ya?ml$/,
type: 'json',
use: 'yaml-loader'
}
]
}
}

View File

@ -1212,9 +1212,9 @@
}
},
"@octokit/openapi-types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz",
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.1.tgz",
"integrity": "sha512-9AuC04PUnZrjoLiw3uPtwGh9FE4Q3rTqs51oNlQ0rkwgE8ftYsOC+lsrQyvCvWm85smBbSc0FNRKKumvGyb44Q==",
"dev": true
},
"@octokit/plugin-enterprise-rest": {
@ -1358,12 +1358,12 @@
}
},
"@octokit/types": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz",
"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.2.tgz",
"integrity": "sha512-LPCpcLbcky7fWfHCTuc7tMiSHFpFlrThJqVdaHgowBTMS0ijlZFfonQC/C1PrZOjD4xRCYgBqH9yttEATGE/nw==",
"dev": true,
"requires": {
"@octokit/openapi-types": "^2.0.0",
"@octokit/openapi-types": "^2.0.1",
"@types/node": ">= 8"
}
},
@ -13038,6 +13038,20 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg=="
},
"yaml-loader": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.6.0.tgz",
"integrity": "sha512-1bNiLelumURyj+zvVHOv8Y3dpCri0F2S+DCcmps0pA1zWRLjS+FhZQg4o3aUUDYESh73+pKZNI18bj7stpReow==",
"requires": {
"loader-utils": "^1.4.0",
"yaml": "^1.8.3"
}
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",

View File

@ -100,7 +100,7 @@ impl Display for IpWithSocket {
}
/// Project name.
#[derive(Debug,Display,Clone,Serialize,Deserialize,PartialEq,Shrinkwrap)]
#[derive(Debug,Display,Clone,Serialize,Deserialize,From,PartialEq,Shrinkwrap)]
#[shrinkwrap(mutable)]
pub struct ProjectName(pub String);

View File

@ -8,26 +8,14 @@ use ensogl::system::web;
// =================
// === Constants ===
// =================
mod connection_arguments {
pub const PROJECT_MANAGER : &str = "project_manager";
pub const LANGUAGE_SERVER_JSON : &str = "language_server_rpc";
pub const LANGUAGE_SERVER_BINARY : &str = "language_server_data";
}
// ==============
// === Errors ===
// ==============
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,Fail)]
#[fail(display="Missing program option: {}.",name)]
pub struct MissingOption {name:&'static str}
#[fail(display="Missing program option: {}.",0)]
pub struct MissingOption (&'static str);
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,Fail)]
@ -36,9 +24,9 @@ pub struct MutuallyExclusiveOptions;
// ==================
// === Connection ===
// ==================
// ======================
// === BackendService ===
// ======================
/// A Configuration defining to what backend service should IDE connect.
#[allow(missing_docs)]
@ -64,36 +52,127 @@ impl Default for BackendService {
impl BackendService {
/// Read backend configuration from the web arguments. See also [`web::Arguments`]
/// documentation.
pub fn from_web_arguments(arguments:&web::Arguments) -> FallibleResult<Self> {
let pm_endpoint = arguments.get(connection_arguments::PROJECT_MANAGER).cloned();
let ls_json_endpoint = arguments.get(connection_arguments::LANGUAGE_SERVER_JSON).cloned();
let ls_bin_endpoint = arguments.get(connection_arguments::LANGUAGE_SERVER_BINARY).cloned();
if let Some(endpoint) = pm_endpoint {
if ls_json_endpoint.is_some() || ls_bin_endpoint.is_some() {
pub fn from_web_arguments(config:&ConfigReader) -> FallibleResult<Self> {
if let Some(endpoint) = &config.project_manager {
if config.language_server_rpc.is_some() || config.language_server_data.is_some() {
Err(MutuallyExclusiveOptions.into())
} else {
let endpoint = endpoint.clone();
Ok(Self::ProjectManager {endpoint})
}
} else {
match (ls_json_endpoint,ls_bin_endpoint) {
(Some(json_endpoint),Some(binary_endpoint)) =>
Ok(Self::LanguageServer {json_endpoint,binary_endpoint}),
(None,None) =>
Ok(default()),
(Some(_),None) =>
Err(MissingOption{name:connection_arguments::LANGUAGE_SERVER_BINARY}.into()),
(None,Some(_)) =>
Err(MissingOption{name:connection_arguments::LANGUAGE_SERVER_JSON }.into())
match (&config.language_server_rpc,&config.language_server_data) {
(Some(json_endpoint),Some(binary_endpoint)) => {
let json_endpoint = json_endpoint.clone();
let binary_endpoint = binary_endpoint.clone();
Ok(Self::LanguageServer {json_endpoint,binary_endpoint})
}
(None,None) => Ok(default()),
(Some(_),None) => Err(MissingOption(config.names().language_server_data).into()),
(None,Some(_)) => Err(MissingOption(config.names().language_server_rpc).into())
}
}
}
}
// ==============
// === Config ===
// ==============
/// The path at which the config is accessible. This needs to be synchronised with the
/// `src/config.yaml` configuration file. In the future, we could write a procedural macro, which
/// loads the configuration and splits Rust variables from it during compilation time. This is not
/// possible by using macro rules, as there is no way to plug in the output of `include_str!` macro
/// to another macro input.
const WINDOW_CFG_PATH : &[&str] = &["enso","config"];
/// Defines a new config structure. The provided fields are converted to optional fields. The config
/// constructor queries JavaScript configuration for the keys defined in this structure. For each
/// resulting string value, it converts it to the defined type. It also reports warnings for all
/// config options that were provided, but were not matched this definition.
macro_rules! define_config {
($name:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
/// Reflection mechanism containing string representation of option names.
#[derive(Clone,Copy,Debug)]
pub struct Names {
$($field : &'static str),*
}
impl Default for Names {
fn default() -> Self {
$(let $field = stringify!{$field};)*
Self {$($field),*}
}
}
/// The structure containing application configs.
#[derive(Clone,Debug,Default)]
pub struct $name {
__names__ : Names,
$($field : Option<$field_type>),*
}
impl $name {
/// Constructor.
pub fn new() -> Self {
let logger = Logger::new(stringify!{$name});
let window = web::window();
match web::reflect_get_nested_object(&window,WINDOW_CFG_PATH).ok() {
None => {
let path = WINDOW_CFG_PATH.join(".");
error!(&logger,"The config path '{path}' is invalid.");
default()
}
Some(cfg) => {
let __names__ = default();
let keys = web::object_keys(&cfg);
let mut keys = keys.into_iter().collect::<HashSet<String>>();
$(
let name = stringify!{$field};
let $field = web::reflect_get_nested_string(&cfg,&[name]).ok();
let $field = $field.map(|t|t.into());
keys.remove(name);
)*
for key in keys {
warning!(&logger,"Unknown config option provided '{key}'.");
}
Self {__names__,$($field),*}
}
}
}
/// Reflection mechanism to get string representation of option names.
pub fn names(&self) -> &Names {
&self.__names__
}
}
};
}
define_config! {
ConfigReader {
entry : String,
project : ProjectName,
project_manager : String,
language_server_rpc : String,
language_server_data : String,
}
}
// ===============
// === Startup ===
// ===============
/// Configuration data necessary to initialize IDE.
#[derive(Clone,Debug)]
pub struct Startup {
/// The configuration of connection to the backend service.
pub backend:BackendService,
pub backend : BackendService,
/// The project name we want to open on startup.
pub project_name : ProjectName
}
@ -110,12 +189,11 @@ impl Default for Startup {
impl Startup {
/// Read configuration from the web arguments. See also [`web::Arguments`] documentation.
pub fn from_web_arguments() -> FallibleResult<Startup> {
let arguments = ensogl::system::web::Arguments::new();
let backend = BackendService::from_web_arguments(&arguments)?;
let project_name = arguments.get("project").map(ProjectName::new);
let project_name = project_name.unwrap_or_else(|| {
let config = ConfigReader::new();
let backend = BackendService::from_web_arguments(&config)?;
let project_name = config.project.unwrap_or_else(||
ProjectName::new(constants::DEFAULT_PROJECT_NAME)
});
);
Ok(Startup{backend,project_name})
}
}

View File

@ -40,7 +40,7 @@ pub use web_sys::WebGl2RenderingContext;
pub use web_sys::Window;
pub use std::time::Duration;
pub use std::time::Instant;
use std::collections::HashMap;
// =============
@ -62,6 +62,13 @@ pub fn Error<S:Into<String>>(message:S) -> Error {
pub type Result<T> = std::result::Result<T,Error>;
impl From<JsValue> for Error {
fn from(t:JsValue) -> Self {
let message = format!("{:?}",t);
Self {message}
}
}
// ==============
@ -595,50 +602,46 @@ pub async fn sleep(duration:Duration) {
#[cfg(not(target_arch = "wasm32"))]
pub use async_std::task::sleep;
/// Stores arguments extracted from `Location`'s search
/// (https://developer.mozilla.org/en-US/docs/Web/API/Location/search).
/// e.g. extracts arg0 = value0, and arg1 = value1 from http://localhost/?arg0=value0&arg1=value1.
#[derive(Debug)]
pub struct Arguments {
pub hash_map : HashMap<String,String>
}
impl Deref for Arguments {
type Target = HashMap<String,String>;
fn deref(&self) -> &Self::Target { &self.hash_map }
}
impl Arguments {
fn args_from_search(search:&str) -> HashMap<String,String> {
if search.chars().nth(0) == Some('?') {
let search_without_question_mark = &search[1..];
search_without_question_mark.split('&').filter_map(|arg| {
match arg.split('=').collect_vec().as_slice() {
[key,value] => Some(((*key).to_string(),(*value).to_string())),
[key] => Some(((*key).to_string(),"".to_string())),
_ => None
}
}).collect()
} else {
default()
}
}
/// Creates a new arguments map from location search.
pub fn new() -> Self {
default()
/// Get the nested value of the provided object. This is similar to writing `foo.bar.baz` in
/// JavaScript, but in a safe manner, while checking if the value exists on each level.
pub fn reflect_get_nested(target:&JsValue, keys:&[&str]) -> Result<JsValue> {
let mut tgt = target.clone();
for key in keys {
let obj = tgt.dyn_into::<js_sys::Object>()?;
let key = (*key).into();
tgt = js_sys::Reflect::get(&obj,&key)?;
}
Ok(tgt)
}
impl Default for Arguments {
fn default() -> Self {
let search = window().location().search().unwrap_or_default();
let hash_map = Self::args_from_search(&search);
Self{hash_map}
}
/// Get the nested value of the provided object and cast it to [`Object`]. See docs of
/// [`reflect_get_nested`] to learn more.
pub fn reflect_get_nested_object(target:&JsValue, keys:&[&str]) -> Result<js_sys::Object> {
let tgt = reflect_get_nested(target,keys)?;
Ok(tgt.dyn_into()?)
}
/// Get the nested value of the provided object and cast it to [`String`]. See docs of
/// [`reflect_get_nested`] to learn more.
pub fn reflect_get_nested_string(target:&JsValue, keys:&[&str]) -> Result<String> {
let tgt = reflect_get_nested(target,keys)?;
let val = tgt.dyn_into::<js_sys::JsString>()?;
Ok(val.into())
}
/// Get all the keys of the provided [`Object`].
pub fn object_keys(target:&JsValue) -> Vec<String> {
target.clone().dyn_into::<js_sys::Object>().ok().map(|obj| {
js_sys::Object::keys(&obj).iter().map(|key| {
// The unwrap is safe, the `Object::keys` API guarantees it.
let js_str = key.dyn_into::<js_sys::JsString>().unwrap();
js_str.into()
}).collect()
}).unwrap_or_default()
}
// ============
// === Test ===
// ============
@ -665,29 +668,6 @@ mod tests {
}
}
#[test]
fn args() {
let search = String::from("?project=HelloWorld&arg0=value0");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 2);
assert_eq!(args.get("project"), Some(&"HelloWorld".to_string()));
assert_eq!(args.get("arg0"), Some(&"value0".to_string()));
let search = String::from("project=HelloWorld&arg0=value0");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 0);
let search = String::from("");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 0);
let search = String::from("?project&arg0=value0");
let args = Arguments::args_from_search(&search);
assert_eq!(args.len(), 2);
assert_eq!(args.get("project"), Some(&"".to_string()));
assert_eq!(args.get("arg0"), Some(&"value0".to_string()));
}
#[cfg(not(target_arch = "wasm32"))]
mod helpers {
use std::time::Instant;