From a5c5762ddac95c827a72c021238b31bc06c821fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wawrzyniec=20Urba=C5=84czyk?= Date: Thu, 29 Apr 2021 23:21:18 +0200 Subject: [PATCH 01/17] Passing options to backend and adding `--verbose` flag (#1531) ref #1525 --- CHANGELOG.md | 6 ++++++ build/run.js | 7 ++++--- src/js/lib/client/src/index.js | 23 ++++++++++++++++++++--- src/rust/ide/lib/args/src/lib.rs | 1 + 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 259f6e4242..ec9d70e2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ - [Window management buttons.][1511]. The IDE now has components for "fullscreen" nad "close" buttons. They will be available only when running IDE in a cloud environment. +- [Customizable backend options][1531]. When invoking Enso IDE through command + line interface, it is possible to give `--` argument separator. All arguments + following the separator will be passed to the backend. +- [Added `--verbose` parameter][1531]. If `--verbose` is given as command line + argument, the IDE and the backend will produce more detailed logs.
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) @@ -23,6 +28,7 @@ you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). [1511]: https://github.com/enso-org/ide/pull/1511 +[1531]: https://github.com/enso-org/ide/pull/1531
diff --git a/build/run.js b/build/run.js index 28b9cbd4a8..874541b9a6 100755 --- a/build/run.js +++ b/build/run.js @@ -206,9 +206,10 @@ commands.start.rust = async function(argv) { commands.start.js = async function (argv) { await installJsDeps() console.log(`Building JS target.` + argv) - const args = targetArgs.concat([ - `--backend-path ${paths.get_project_manager_path(paths.dist.bin)}`, - ]) + // The backend path is being prepended here, as appending would be incorrect. + // That is because `targetArgs` might include `-- …` and appended args could + // end up being passed to the spawned backend process. + const args = ['--backend-path', paths.get_project_manager_path(paths.dist.bin)].concat(targetArgs) if (argv.dev) { args.push('--dev') } await cmd.with_cwd(paths.js.root, async () => { await run('npm', ['run', 'start', '--'].concat(args)) diff --git a/src/js/lib/client/src/index.js b/src/js/lib/client/src/index.js index 19eb677224..c6a11d5aed 100644 --- a/src/js/lib/client/src/index.js +++ b/src/js/lib/client/src/index.js @@ -61,12 +61,17 @@ const execFile = util.promisify(child_process.execFile); let usage = ` ${pkg.build.productName} ${rootCfg.version} command line interface. -Usage: ${pkg.build.productName} [options] +Usage: ${pkg.build.productName} [options] [--] [backend args]... ` +let epilogue = ` +Arguments that follow the two dashes (\`--\`) will be passed to the backend process. They are used\ + if IDE spawns backend, i.e. if '--backend false' has not been set.` + let optParser = yargs .scriptName("") .usage(usage) + .epilogue(epilogue) .help() .version(false) .parserConfiguration({'populate--':true}) @@ -116,6 +121,13 @@ optParser.options('backend-path', { let debugOptionsGroup = 'Debug Options:' +optParser.options('verbose', { + group : debugOptionsGroup, + describe : `Increase logs verbosity. Affects both IDE and the backend.`, + default : false, + type : `boolean` +}) + optParser.options('entry-point', { group : debugOptionsGroup, describe : 'Run an alternative entry point (e.g. one of the debug scenes)', @@ -401,8 +413,12 @@ function spawnProjectManager(args) { function runBackend() { if(args.backend !== false) { - console.log("Starting the backend process.") - return spawnProjectManager() + let opts = args['--'] ? args['--'] : [] + if(args.verbose === true) { + opts.push('-vv') + } + console.log("Starting the backend process with the following options:", opts) + return spawnProjectManager(opts) } } @@ -504,6 +520,7 @@ function createWindow() { crash_report_host : args.crashReportHost, no_data_gathering : args.noDataGathering, node_labels : args.nodeLabels, + verbose : args.verbose, } if (args.project) { urlCfg.project = args.project } diff --git a/src/rust/ide/lib/args/src/lib.rs b/src/rust/ide/lib/args/src/lib.rs index 95fc55e61b..ace721d6ba 100644 --- a/src/rust/ide/lib/args/src/lib.rs +++ b/src/rust/ide/lib/args/src/lib.rs @@ -39,5 +39,6 @@ ensogl::read_args! { crash_report_host : String, no_data_gathering : bool, is_in_cloud : bool, + verbose : bool, } } From 9d40adeb62be328883317e8322e996290ce1c94a Mon Sep 17 00:00:00 2001 From: Stanislav Date: Fri, 30 Apr 2021 15:21:43 +0200 Subject: [PATCH 02/17] Update changelog parser to respect rc (#1535) --- build/release.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/build/release.js b/build/release.js index bb11362e47..387ee6f0f6 100644 --- a/build/release.js +++ b/build/release.js @@ -33,12 +33,14 @@ class NextReleaseVersion { } class Version { - constructor(major,minor,patch,tag,tagVersion) { - this.major = major - this.minor = minor - this.patch = patch - this.tag = tag - this.tagVersion = tagVersion + constructor(major,minor,patch,tag,tagVersion,rcTag,rcTagVersion) { + this.major = major + this.minor = minor + this.patch = patch + this.tag = tag + this.tagVersion = tagVersion + this.rcTag = rcTag + this.rcTagVersion = rcTagVersion } lt(that) { @@ -48,7 +50,8 @@ class Version { if (this.tag === 'alpha' && that.tag === 'beta') { return true } if (this.tag === 'alpha' && that.tag === 'rc') { return true } if (this.tag === 'beta' && that.tag === 'rc') { return true } - if (this.tagVersion < that.tagVersion) { return true } + if (this.tagVersion < that.tagVersion) { return true } + if (this.rcTagVersion < that.rcTagVersion) { return true } return false } @@ -60,6 +63,9 @@ class Version { let suffix = '' if (this.tag) { suffix = `-${this.tag}.${this.tagVersion}` + if (this.rcTag) { + suffix += `.${this.rcTag}.${this.rcTagVersion}` + } } return `${this.major}.${this.minor}.${this.patch}${suffix}` } @@ -140,13 +146,13 @@ function changelogEntries() { let version = new NextReleaseVersion entries.push(new ChangelogEntry(version,body)) } else { - let headerReg = /^ Enso (?[0-9]+)\.(?[0-9]+)\.(?[0-9]+)(-(?alpha|beta|rc)\.(?[0-9]+))? \((?[0-9][0-9][0-9][0-9])-(?[0-9][0-9])-(?[0-9][0-9])\)/ + let headerReg = /^ Enso (?[0-9]+)\.(?[0-9]+)\.(?[0-9]+)(-(?alpha|beta|rc)\.(?[0-9]+))?(.(?rc)\.(?[0-9]+))? \((?[0-9][0-9][0-9][0-9])-(?[0-9][0-9])-(?[0-9][0-9])\)/ let match = header.match(headerReg) if (!match) { throw `Improper changelog entry header: '${header}'. See the 'CHANGELOG_TEMPLATE.md' for details.` } let grps = match.groups - let version = new Version(grps.major,grps.minor,grps.patch,grps.tag,grps.tagVersion) + let version = new Version(grps.major,grps.minor,grps.patch,grps.tag,grps.tagVersion,grps.rcTag,grps.rcTagVersion) entries.push(new ChangelogEntry(version,body)) } firstSection = false From d659d6e29500bae876d9e85fcb8ab6015d9f1f38 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 4 May 2021 11:12:59 +0200 Subject: [PATCH 03/17] Correctly interpret string arguments as booleans in electron arguments. (#1536) --- CHANGELOG.md | 5 +++++ src/js/lib/content/src/index.js | 39 ++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9d70e2e2..f19a24d6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) +- [Fix some internal settings not being applied correctly in the IDE][1536]. + Some arguments were not passed correctly to the IDE leading to erroneous + behaviour in the electron app. This is now fixed. + #### Visual Environment #### EnsoGL (rendering engine) @@ -28,6 +32,7 @@ you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). [1511]: https://github.com/enso-org/ide/pull/1511 +[1511]: https://github.com/enso-org/ide/pull/1536 [1531]: https://github.com/enso-org/ide/pull/1531
diff --git a/src/js/lib/content/src/index.js b/src/js/lib/content/src/index.js index 48b8b6a8ea..bd239b9d05 100644 --- a/src/js/lib/content/src/index.js +++ b/src/js/lib/content/src/index.js @@ -7,6 +7,7 @@ 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' import cfg from '../../../config' +import assert from "assert"; @@ -438,6 +439,36 @@ function ok(value) { return value !== null && value !== undefined } +/// Check whether the value is a string with value `"true"`/`"false"`, if so, return the +// appropriate boolean instead. Otherwise, return the original value. +function parseBooleanOrLeaveAsIs(value) { + if (value === "true"){ + return true + } + if (value === "false"){ + return false + } + return value +} + +/// Turn all values that have a boolean in string representation (`"true"`/`"false"`) into actual +/// booleans (`true/`false``). +function parseAllBooleans(config) { + for (const key in config) { + config[key] = parseBooleanOrLeaveAsIs(config[key]) + } +} + +function initLogging(config) { + assert(typeof config.no_data_gathering == "boolean") + if (config.no_data_gathering ) { + API.remoteLog = function (_event, _data) {} + } else { + let logger = new MixpanelLogger + API.remoteLog = function (event,data) {logger.log(event,data)} + } +} + /// Main entry point. Loads WASM, initializes it, chooses the scene to run. API.main = async function (inputConfig) { let defaultConfig = { @@ -451,14 +482,10 @@ API.main = async function (inputConfig) { let urlParams = new URLSearchParams(window.location.search); let urlConfig = Object.fromEntries(urlParams.entries()) let config = Object.assign(defaultConfig,inputConfig,urlConfig) + parseAllBooleans(config) API[globalConfig.windowAppScopeConfigName] = config - if (config.no_data_gathering) { - API.remoteLog = function (_event, _data) {} - } else { - let logger = new MixpanelLogger - API.remoteLog = function (event,data) {logger.log(event,data)} - } + initLogging(config) window.setInterval(() =>{API.remoteLog("alive");}, ALIVE_LOG_INTERVAL) //Build data injected during the build process. See `webpack.config.js` for the source. From d8c30eac31187e036a4b5dee0b7d022ae408bf0e Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 4 May 2021 12:54:13 +0200 Subject: [PATCH 04/17] Update changelog for release of Enso 2.0.0-alpha.4. --- CHANGELOG.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f19a24d6c2..264fff5573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,25 @@ -# Next Release - -
![New Learning Resources](/docs/assets/tags/new_learning_resources.svg) +# Enso 2.0.0-alpha.4 (2021-05-04)
![New Features](/docs/assets/tags/new_features.svg) #### Visual Environment - [Window management buttons.][1511]. The IDE now has components for - "fullscreen" nad "close" buttons. They will be available only when running IDE - in a cloud environment. + "fullscreen" and "close" buttons. They will when running IDE in a cloud + environment where no native window buttons are available. - [Customizable backend options][1531]. When invoking Enso IDE through command - line interface, it is possible to give `--` argument separator. All arguments - following the separator will be passed to the backend. + line interface, it is possible to add the `--` argument separator. All + arguments following the separator will be passed to the backend. - [Added `--verbose` parameter][1531]. If `--verbose` is given as command line argument, the IDE and the backend will produce more detailed logs.
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) -- [Fix some internal settings not being applied correctly in the IDE][1536]. - Some arguments were not passed correctly to the IDE leading to erroneous - behaviour in the electron app. This is now fixed. - #### Visual Environment -#### EnsoGL (rendering engine) +- [Some command line arguments were not applied correctly in the IDE][1536]. + Some arguments were not passed correctly to the IDE leading to erroneous + behavior or appearance of the electron app. This is now fixed. #### Enso Compiler @@ -32,7 +28,7 @@ you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). [1511]: https://github.com/enso-org/ide/pull/1511 -[1511]: https://github.com/enso-org/ide/pull/1536 +[1536]: https://github.com/enso-org/ide/pull/1536 [1531]: https://github.com/enso-org/ide/pull/1531
From fc9454a2d66a749bb2c0eff494210f1157b55b54 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 4 May 2021 12:54:13 +0200 Subject: [PATCH 05/17] Update changelog for release of Enso 2.0.0-alpha.4. --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f19a24d6c2..3ef1f83115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,24 +6,45 @@ #### Visual Environment +#### EnsoGL (rendering engine) + + +
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) + +#### Visual Environment + +#### EnsoGL (rendering engine) + +#### Enso Compiler + +If you're interested in the enhancements and fixes made to the Enso compiler, +you can find their release notes +[here](https://github.com/enso-org/enso/blob/main/RELEASES.md). + +
+ +# Enso 2.0.0-alpha.4 (2021-05-04) + +
![New Features](/docs/assets/tags/new_features.svg) + +#### Visual Environment + - [Window management buttons.][1511]. The IDE now has components for - "fullscreen" nad "close" buttons. They will be available only when running IDE - in a cloud environment. + "fullscreen" and "close" buttons. They will when running IDE in a cloud + environment where no native window buttons are available. - [Customizable backend options][1531]. When invoking Enso IDE through command - line interface, it is possible to give `--` argument separator. All arguments - following the separator will be passed to the backend. + line interface, it is possible to add the `--` argument separator. All + arguments following the separator will be passed to the backend. - [Added `--verbose` parameter][1531]. If `--verbose` is given as command line argument, the IDE and the backend will produce more detailed logs.
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) -- [Fix some internal settings not being applied correctly in the IDE][1536]. - Some arguments were not passed correctly to the IDE leading to erroneous - behaviour in the electron app. This is now fixed. - #### Visual Environment -#### EnsoGL (rendering engine) +- [Some command line arguments were not applied correctly in the IDE][1536]. + Some arguments were not passed correctly to the IDE leading to erroneous + behavior or appearance of the electron app. This is now fixed. #### Enso Compiler @@ -32,7 +53,7 @@ you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). [1511]: https://github.com/enso-org/ide/pull/1511 -[1511]: https://github.com/enso-org/ide/pull/1536 +[1536]: https://github.com/enso-org/ide/pull/1536 [1531]: https://github.com/enso-org/ide/pull/1531
From 82534396c7f3724ea43ee157f91d59712e3e8dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Dani=C5=82o?= Date: Fri, 7 May 2021 04:11:56 +0200 Subject: [PATCH 06/17] Update CONTRIBUTING.md --- docs/CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ef65aa086d..23d8957807 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -59,10 +59,10 @@ setup: install it: ```bash - rustup toolchain install nightly-2019-11-04 # Install the nightly channel. - rustup component add clippy # Install the linter. - cargo install wasm-pack --version 0.9.1 # Install the wasm-pack toolkit. - cargo install cargo-watch # To enable ./run watch utility + rustup toolchain install nightly-2019-11-04 # Install the nightly channel. + rustup component add clippy # Install the linter. + cargo +stable install wasm-pack --version 0.9.1 # Install the wasm-pack toolkit. + cargo +stable install cargo-watch # To enable ./run watch utility ``` - **Node and Node Package Manager LTS** From 70ce429e8b8af5db72bba737355d470ff83b167e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wawrzyniec=20Urba=C5=84czyk?= Date: Fri, 7 May 2021 11:27:20 +0200 Subject: [PATCH 07/17] Delete key shall delete nodes. (#1538) --- CHANGELOG.md | 5 ++++- docs/product/shortcuts.md | 2 +- src/rust/ide/view/graph-editor/src/lib.rs | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef1f83115..989fae8878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ #### EnsoGL (rendering engine) -
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) #### Visual Environment +- [Delete key will delete selected nodes][1538]. + #### EnsoGL (rendering engine) #### Enso Compiler @@ -21,6 +22,8 @@ If you're interested in the enhancements and fixes made to the Enso compiler, you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). +[1538]: https://github.com/enso-org/ide/pull/1538 +
# Enso 2.0.0-alpha.4 (2021-05-04) diff --git a/docs/product/shortcuts.md b/docs/product/shortcuts.md index d46eb266cd..4040a03afa 100644 --- a/docs/product/shortcuts.md +++ b/docs/product/shortcuts.md @@ -83,7 +83,7 @@ further investigation. | Shortcut | Action | | -------- | ------ | | tab | Show / hide node searcher. | -| backspace | Remove selected nodes. | +| backspace or delete | Remove selected nodes. | | cmd+g | Collapse (group) selected nodes. | | meta+LMB | Start editing node expression. | | meta+enter | Start editing node expression. | diff --git a/src/rust/ide/view/graph-editor/src/lib.rs b/src/rust/ide/view/graph-editor/src/lib.rs index 52cd599ea7..2652336702 100644 --- a/src/rust/ide/view/graph-editor/src/lib.rs +++ b/src/rust/ide/view/graph-editor/src/lib.rs @@ -1908,6 +1908,7 @@ impl application::View for GraphEditor { (Press , "" , "left-mouse-button" , "node_press") , (Release , "" , "left-mouse-button" , "node_release") , (Press , "!node_editing" , "backspace" , "remove_selected_nodes") + , (Press , "!node_editing" , "delete" , "remove_selected_nodes") , (Press , "" , "cmd g" , "collapse_selected_nodes") // === Visualization === From f480b9b6fb73bbca6834adc96af3eadc79c0ab19 Mon Sep 17 00:00:00 2001 From: Ara Adkins Date: Fri, 7 May 2021 15:01:12 +0100 Subject: [PATCH 08/17] Add issue link configuration (#1558) --- .github/ISSUE_TEMPLATE/config.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..29465522e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Issues with Enso? + url: https://github.com/enso-org/enso/issues/new/choose + about: Please report problems with the Enso in the Enso repository. + - name: Have a question about Enso? + url: https://github.com/enso-org/enso/discussions/new?category=Questions + about: Ask your questions about Enso + - name: Want to discuss Enso? + url: https://github.com/enso-org/enso/discussions/new?category=general + about: Talk about Enso the language + - name: Have an idea? + url: https://github.com/enso-org/enso/discussions/new?category=ideas + about: Share your ideas for Enso From a185215c716c3ac267e840312c5d6dbe73b4c59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wawrzyniec=20Urba=C5=84czyk?= Date: Mon, 10 May 2021 09:42:59 +0200 Subject: [PATCH 09/17] Reconnecting WebSocket (#1537) --- src/rust/ide/Cargo.toml | 5 +- src/rust/ide/lib/json-rpc/src/transport.rs | 1 + src/rust/ide/src/executor/test_utils.rs | 9 +- src/rust/ide/src/executor/web.rs | 2 + src/rust/ide/src/executor/web/test.rs | 8 + src/rust/ide/src/ide/initializer.rs | 4 +- src/rust/ide/src/transport/web.rs | 321 ++++++++++++++---- .../foreign/java_script/instance.rs | 2 +- .../lib/system/web/src/closure/storage.rs | 26 +- src/rust/lib/system/web/src/event.rs | 51 +++ src/rust/lib/system/web/src/event/listener.rs | 117 +++++++ src/rust/lib/system/web/src/lib.rs | 15 +- 12 files changed, 482 insertions(+), 79 deletions(-) create mode 100644 src/rust/ide/src/executor/web/test.rs create mode 100644 src/rust/lib/system/web/src/event.rs create mode 100644 src/rust/lib/system/web/src/event/listener.rs diff --git a/src/rust/ide/Cargo.toml b/src/rust/ide/Cargo.toml index dc02b56fa6..9e955ce720 100644 --- a/src/rust/ide/Cargo.toml +++ b/src/rust/ide/Cargo.toml @@ -62,8 +62,9 @@ features = [ 'CloseEvent', 'Document', 'Element', - "ErrorEvent", - "MessageEvent", + 'ErrorEvent', + 'EventTarget', + 'MessageEvent', 'HtmlElement', 'Node', 'WebSocket', diff --git a/src/rust/ide/lib/json-rpc/src/transport.rs b/src/rust/ide/lib/json-rpc/src/transport.rs index 65418488c4..e22b86711d 100644 --- a/src/rust/ide/lib/json-rpc/src/transport.rs +++ b/src/rust/ide/lib/json-rpc/src/transport.rs @@ -42,5 +42,6 @@ pub enum TransportEvent { /// A socket has been opened. Opened, /// A socket has been closed by the peer. + /// This event may be also emitted when reconnecting has failed. Closed, } diff --git a/src/rust/ide/src/executor/test_utils.rs b/src/rust/ide/src/executor/test_utils.rs index 695ea83e0d..cc84d89e4b 100644 --- a/src/rust/ide/src/executor/test_utils.rs +++ b/src/rust/ide/src/executor/test_utils.rs @@ -29,6 +29,11 @@ impl TestWithLocalPoolExecutor { Self {executor,running_task_count} } + /// Check if there are any uncompleted tasks in the pool. + pub fn has_ongoing_task(&self) -> bool { + self.running_task_count.get() > 0 + } + /// Spawn new task in executor. pub fn run_task(&mut self, task: Task) where Task : Future + 'static { @@ -47,7 +52,7 @@ impl TestWithLocalPoolExecutor { pub fn when_stalled(&mut self, callback:Callback) where Callback : FnOnce() { self.run_until_stalled(); - if self.running_task_count.get() > 0 { + if self.has_ongoing_task() { callback(); } } @@ -59,7 +64,7 @@ impl TestWithLocalPoolExecutor { pub fn when_stalled_run_task(&mut self, task : Task) where Task : Future + 'static { self.run_until_stalled(); - if self.running_task_count.get() > 0 { + if self.has_ongoing_task() { self.run_task(task); } } diff --git a/src/rust/ide/src/executor/web.rs b/src/rust/ide/src/executor/web.rs index 6c46d296ae..2f62ab052c 100644 --- a/src/rust/ide/src/executor/web.rs +++ b/src/rust/ide/src/executor/web.rs @@ -1,6 +1,8 @@ //! Module defining `JsExecutor` - an executor that tries running until stalled //! on each animation frame callback call. +pub mod test; + use crate::prelude::*; use ensogl::control::callback; diff --git a/src/rust/ide/src/executor/web/test.rs b/src/rust/ide/src/executor/web/test.rs new file mode 100644 index 0000000000..7e0c649eff --- /dev/null +++ b/src/rust/ide/src/executor/web/test.rs @@ -0,0 +1,8 @@ +//! Utilities for tests related to the web-based executors. + +/// Set up a global animation-frame-based executor. +/// Leaks it handle so it will run indefinitely. +/// To be used in asynchronous wasm tests. +pub fn setup_and_forget() { + std::mem::forget(crate::initializer::setup_global_executor()); +} diff --git a/src/rust/ide/src/ide/initializer.rs b/src/rust/ide/src/ide/initializer.rs index a132f23785..6ddb783c24 100644 --- a/src/rust/ide/src/ide/initializer.rs +++ b/src/rust/ide/src/ide/initializer.rs @@ -279,8 +279,8 @@ async fn create_project_model ) -> FallibleResult { info!(logger, "Establishing Language Server connection."); let client_id = Uuid::new_v4(); - let json_ws = WebSocket::new_opened(logger,json_endpoint).await?; - let binary_ws = WebSocket::new_opened(logger,binary_endpoint).await?; + let json_ws = WebSocket::new_opened(logger,&json_endpoint).await?; + let binary_ws = WebSocket::new_opened(logger,&binary_endpoint).await?; let client_json = language_server::Client::new(json_ws); let client_binary = binary::Client::new(logger,binary_ws); crate::executor::global::spawn(client_json.runner()); diff --git a/src/rust/ide/src/transport/web.rs b/src/rust/ide/src/transport/web.rs index ed9e39f4b2..20104fd6fa 100644 --- a/src/rust/ide/src/transport/web.rs +++ b/src/rust/ide/src/transport/web.rs @@ -2,8 +2,8 @@ use crate::prelude::*; -use ensogl_system_web::closure::storage::OptionalFmMutClosure; use ensogl_system_web::js_to_string; +use ensogl_system_web::event::listener::Slot; use failure::Error; use futures::channel::mpsc; use json_rpc::Transport; @@ -11,9 +11,6 @@ use json_rpc::TransportEvent; use utils::channel; use wasm_bindgen::JsCast; use web_sys::BinaryType; -use web_sys::CloseEvent; -use web_sys::Event; -use web_sys::MessageEvent; @@ -36,6 +33,14 @@ pub enum ConnectingError { FailedToConnect, } +impl ConnectingError { + /// Create a `ConstructionError` value from a JS value describing an error. + pub fn construction_error(js_val:impl AsRef) -> Self { + let text = js_to_string(js_val); + ConnectingError::ConstructionError(text) + } +} + /// Error that may occur when attempting to send the data over WebSocket /// transport. #[derive(Clone,Debug,Fail)] @@ -51,7 +56,7 @@ pub enum SendingError { impl SendingError { /// Constructs from the error yielded by one of the JS's WebSocket sending functions. pub fn from_send_error(error:JsValue) -> SendingError { - SendingError::FailedToSend(js_to_string(error)) + SendingError::FailedToSend(js_to_string(&error)) } } @@ -97,54 +102,204 @@ impl State { +// ================= +// === JS Events === +// ================= + +/// Description of events that can be emitted by JS WebSocket. +pub mod event { + use super::*; + use ensogl_system_web::event::Type; + + /// Represents WebSocket.open event. + #[derive(Clone,Copy,Debug)] + pub enum Open{} + impl Type for Open { + type Interface = web_sys::Event; + type Target = web_sys::WebSocket; + const NAME:&'static str = "open"; + } + + /// Represents WebSocket.close event. + #[derive(Clone,Copy,Debug)] + pub enum Close{} + impl Type for Close { + type Interface = web_sys::CloseEvent; + type Target = web_sys::WebSocket; + const NAME:&'static str = "close"; + } + + /// Represents WebSocket.message event. + #[derive(Clone,Copy,Debug)] + pub enum Message{} + impl Type for Message { + type Interface = web_sys::MessageEvent; + type Target = web_sys::WebSocket; + const NAME:&'static str = "message"; + } + + /// Represents WebSocket.error event. + #[derive(Clone,Copy,Debug)] + pub enum Error{} + impl Type for Error { + type Interface = web_sys::Event; + type Target = web_sys::WebSocket; + const NAME:&'static str = "error"; + } +} + + + +// ============= +// === Model === +// ============= + +/// An owning wrapper over JS `WebSocket` object and callbacks to its signals. +#[derive(Derivative)] +#[derivative(Debug)] +#[allow(missing_docs)] +struct Model { + // === User-provided callbacks === + pub on_close : Slot, + pub on_message : Slot, + pub on_open : Slot, + pub on_error : Slot, + + + // === Internal === + pub logger : Logger, + pub socket : web_sys::WebSocket, + /// Special callback on "close" event. As it must be invoked after `on_close`, care should be + /// taken to keep it registered as an event listener *after* `on_close` registration. + /// By default `Model` takes care of it by itself. + pub on_close_internal : Slot, + /// When enabled, the WS will try to automatically reconnect whenever connection is lost. + pub auto_reconnect : bool, +} + +impl Model { + /// Wraps given WebSocket object. + pub fn new(socket:web_sys::WebSocket, logger:Logger) -> Model { + socket.set_binary_type(BinaryType::Arraybuffer); + Model { + on_close : Slot::new(&socket, &logger), + on_message : Slot::new(&socket, &logger), + on_open : Slot::new(&socket, &logger), + on_error : Slot::new(&socket, &logger), + on_close_internal : Slot::new(&socket, &logger), + auto_reconnect : true, + logger, + socket, + } + } + + /// Close the socket. + pub fn close(&mut self, reason:&str) -> Result<(),JsValue> { + // If socket was manually requested to close, it should not try to reconnect then. + self.auto_reconnect = false; + let normal_closure = 1000; + self.socket.close_with_code_and_reason(normal_closure,reason)?; + self.clear_callbacks(); + Ok(()) + } + + /// Clear all the available callbacks. + pub fn clear_callbacks(&mut self) { + // We list explicitly all the fields, to get a compiler error when a new slot as added + // but not handled here. + #[allow(clippy::unneeded_field_pattern)] + let Self{ + // Callback slots to be cleared. + on_close, on_error, on_message, on_open, on_close_internal, + // Explicitly ignored non-slot fields. + auto_reconnect:_, logger:_, socket:_ + } = self; + // We don't care if removing actually removed anything. + // If callbacks were not set, then they are clear from the start. + on_close.clear_callback(); + on_error.clear_callback(); + on_message.clear_callback(); + on_open.clear_callback(); + on_close_internal.clear_callback() + } + + /// Establish a new WS connection, using the same URL as the previous one. + /// All callbacks will be transferred to the new connection. + pub fn reconnect(&mut self) -> Result<(),JsValue> { + if !self.auto_reconnect { + return Err(js_sys::Error::new("Reconnecting has been disabled").into()); + } + + let url = self.socket.url(); + info!(self.logger, "Reconnecting WS to {url}."); + + let new_ws = web_sys::WebSocket::new(&url)?; + + self.on_close. set_target(&new_ws); + self.on_error. set_target(&new_ws); + self.on_message. set_target(&new_ws); + self.on_open. set_target(&new_ws); + self.on_close_internal.set_target(&new_ws); + self.socket = new_ws; + + Ok(()) + } +} + +impl Drop for Model { + fn drop(&mut self) { + info!(self.logger, "Dropping WS model."); + if let Err(e) = self.close("Rust Value has been dropped.") { + error!(self.logger,"Error when closing socket due to being dropped: {js_to_string(&e)}") + } + } +} + + + // ================= // === WebSocket === // ================= -/// Wrapper over JS `WebSocket` object and callbacks to its signals. -#[derive(Debug)] +/// Wrapper over JS `WebSocket` meant for general use. +#[derive(Clone,CloneRef,Debug)] pub struct WebSocket { #[allow(missing_docs)] - pub logger : Logger, - /// Handle to the JS `WebSocket` object. - pub ws : web_sys::WebSocket, - /// Handle to a closure connected to `WebSocket.onmessage`. - pub on_message : OptionalFmMutClosure, - /// Handle to a closure connected to `WebSocket.onclose`. - pub on_close : OptionalFmMutClosure, - /// Handle to a closure connected to `WebSocket.onopen`. - pub on_open : OptionalFmMutClosure, - /// Handle to a closure connected to `WebSocket.onerror`. - pub on_error : OptionalFmMutClosure, + pub logger : Logger, + model : Rc>, } impl WebSocket { - /// Wraps given WebSocket object. - pub fn new - (ws:web_sys::WebSocket, parent:impl AnyLogger, name:impl AsRef) -> WebSocket { - ws.set_binary_type(BinaryType::Arraybuffer); - WebSocket { - ws, - logger : Logger::sub(parent,name), - on_message : default(), - on_close : default(), - on_open : default(), - on_error : default(), - } + /// Wrap given raw JS WebSocket object. + pub fn new(ws:web_sys::WebSocket, parent:impl AnyLogger) -> WebSocket { + let logger = Logger::sub(parent,ws.url()); + let model = Rc::new(RefCell::new(Model::new(ws,logger.clone()))); + WebSocket {logger,model} } /// Establish connection with endpoint defined by the given URL and wrap it. /// Asynchronous, because it waits until connection is established. pub async fn new_opened - (parent:impl AnyLogger, url:impl Str) -> Result { - let ws = web_sys::WebSocket::new(url.as_ref()).map_err(|e| { - ConnectingError::ConstructionError(js_to_string(e)) - })?; - let mut wst = WebSocket::new(ws,&parent,url.into()); + (parent:impl AnyLogger, url:&str) -> Result { + let ws = web_sys::WebSocket::new(url).map_err(ConnectingError::construction_error)?; + let mut wst = WebSocket::new(ws,&parent); wst.wait_until_open().await?; Ok(wst) } + /// Generate a callback to be invoked when socket needs reconnecting. + fn reconnect_trigger(&self) -> impl FnMut(web_sys::CloseEvent) { + let model = Rc::downgrade(&self.model); + let logger = self.logger.clone(); + move |_| { + if let Some(model) = model.upgrade() { + if let Err(e) = model.borrow_mut().reconnect() { + error!(logger,"Failed to reconnect: {js_to_string(&e)}"); + } + } + } + } + /// Awaits until `open` signal has been emitted. Clears any callbacks on /// this `WebSocket`, if any has been set. async fn wait_until_open(&mut self) -> Result<(),ConnectingError> { @@ -152,6 +307,7 @@ impl WebSocket { // We shall wait for whatever comes first. let (transmitter, mut receiver) = mpsc::unbounded::>(); let transmitter_clone = transmitter.clone(); + self.set_on_close(move |_| { // Note [mwu] Ignore argument, `CloseEvent` here contains rubbish // anyway, nothing useful to pass to caller. Error code or reason @@ -164,7 +320,8 @@ impl WebSocket { match receiver.next().await { Some(Ok(())) => { - self.clear_callbacks(); + self.model.borrow_mut().clear_callbacks(); + self.model.borrow_mut().on_close_internal.set_callback(self.reconnect_trigger()); info!(self.logger, "Connection opened."); Ok(()) } @@ -174,43 +331,35 @@ impl WebSocket { /// Checks the current state of the connection. pub fn state(&self) -> State { - State::query_ws(&self.ws) + State::query_ws(&self.model.borrow().socket) + } + + fn with_borrow_mut_model(&mut self, f:impl FnOnce(&mut Model) -> R) -> R { + with(self.model.borrow_mut(), |mut model| f(model.deref_mut())) } /// Sets callback for the `close` event. - pub fn set_on_close(&mut self, f:impl FnMut(CloseEvent) + 'static) { - self.on_close.wrap(f); - self.ws.set_onclose(self.on_close.js_ref()); + pub fn set_on_close(&mut self, f:impl FnMut(web_sys::CloseEvent) + 'static) { + self.with_borrow_mut_model(move |model| { + model.on_close.set_callback(f); + // Force internal callback to be after the user-defined one. + model.on_close_internal.reattach(); + }); } /// Sets callback for the `error` event. - pub fn set_on_error(&mut self, f:impl FnMut(Event) + 'static) { - self.on_error.wrap(f); - self.ws.set_onerror(self.on_error.js_ref()); + pub fn set_on_error(&mut self, f:impl FnMut(web_sys::Event) + 'static) { + self.with_borrow_mut_model(move |model| model.on_error.set_callback(f)) } /// Sets callback for the `message` event. - pub fn set_on_message(&mut self, f:impl FnMut(MessageEvent) + 'static) { - self.on_message.wrap(f); - self.ws.set_onmessage(self.on_message.js_ref()); + pub fn set_on_message(&mut self, f:impl FnMut(web_sys::MessageEvent) + 'static) { + self.with_borrow_mut_model(move |model| model.on_message.set_callback(f)) } /// Sets callback for the `open` event. - pub fn set_on_open(&mut self, f:impl FnMut(Event) + 'static) { - self.on_open.wrap(f); - self.ws.set_onopen(self.on_open.js_ref()); - } - - /// Clears all the available callbacks. - pub fn clear_callbacks(&mut self) { - self.on_close .clear(); - self.on_error .clear(); - self.on_message.clear(); - self.on_open .clear(); - self.ws.set_onclose(None); - self.ws.set_onerror(None); - self.ws.set_onmessage(None); - self.ws.set_onopen(None); + pub fn set_on_open(&mut self, f:impl FnMut(web_sys::Event) + 'static) { + self.with_borrow_mut_model(move |model| model.on_open.set_callback(f)) } /// Executes a given function with a mutable reference to the socket. @@ -218,7 +367,9 @@ impl WebSocket { /// /// Fails if the socket is not opened or if the sending function failed. /// The error from `F` shall be translated into `SendingError`. - pub fn send_with_open_socket(&mut self, f:F) -> Result + /// + /// WARNING: `f` works under borrow_mut and must not give away control. + fn send_with_open_socket(&mut self, f:F) -> Result where F : FnOnce(&mut web_sys::WebSocket) -> Result { // Sending through the closed WebSocket can return Ok() with error only // appearing in the log. We explicitly check for this to get failure as @@ -230,7 +381,7 @@ impl WebSocket { if state != State::Open { Err(SendingError::NotOpen(state).into()) } else { - let result = f(&mut self.ws); + let result = f(&mut self.model.borrow_mut().socket); result.map_err(|e| SendingError::from_send_error(e).into()) } } @@ -238,13 +389,13 @@ impl WebSocket { impl Transport for WebSocket { fn send_text(&mut self, message:&str) -> Result<(), Error> { - info!(self.logger, "Sending text message of length {message.len()}"); + info!(self.logger, "Sending text message of length {message.len()}."); debug!(self.logger, "Message contents: {message}"); self.send_with_open_socket(|ws| ws.send_with_str(message)) } fn send_binary(&mut self, message:&[u8]) -> Result<(), Error> { - info!(self.logger, "Sending binary message of length {message.len()}"); + info!(self.logger, "Sending binary message of length {message.len()}."); debug!(self.logger,|| format!("Message contents: {:x?}", message)); // TODO [mwu] // Here we workaround issue from wasm-bindgen 0.2.58: @@ -291,3 +442,43 @@ impl Transport for WebSocket { }); } } + +#[cfg(test)] +mod tests { + use super::*; + + use ensogl::system::web; + use std::time::Duration; + + + /// Provisional code allowing testing WS behavior and its events. + /// Keeping it for future debug purposes. + /// To run uncomment attribute line and invoke: + /// `cargo watch -- wasm-pack test .\ide\ --chrome -- websocket_test` + //#[wasm_bindgen_test::wasm_bindgen_test] + #[allow(dead_code)] + async fn websocket_tests() { + web::set_stdout(); + executor::web::test::setup_and_forget(); + let logger = DefaultTraceLogger::new("Test"); + info!(logger,"Started"); + + // Create WebSocket + let ws = WebSocket::new_opened(&logger,"ws://localhost:30445").await; + let mut ws = ws.expect("Couldn't connect to WebSocket server."); + info!(logger,"WebSocket opened: {ws:?}"); + + // Log events + let handler = ws.establish_event_stream().for_each(f!([logger](event) { + info!(logger,"Socket emitted event: {event:?}"); + futures::future::ready(()) + })); + + // Spawn task to process events stream. + executor::global::spawn(handler); + + // Close socket after some delay. + web::sleep(Duration::from_secs(20)).await; + info!(logger,"Finished"); + } +} diff --git a/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs b/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs index 3e236cf32f..165210705e 100644 --- a/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs +++ b/src/rust/ide/view/graph-editor/src/component/visualization/foreign/java_script/instance.rs @@ -277,7 +277,7 @@ impl Instance { use enso_frp::web::js_to_string; let logger = self.model.logger.clone(); error!(logger,"Failed to trigger initial preprocessor update from JS: \ - {js_to_string(js_error)}"); + {js_to_string(&js_error)}"); } self } diff --git a/src/rust/lib/system/web/src/closure/storage.rs b/src/rust/lib/system/web/src/closure/storage.rs index a44cf38864..96f2bc0c13 100644 --- a/src/rust/lib/system/web/src/closure/storage.rs +++ b/src/rust/lib/system/web/src/closure/storage.rs @@ -32,8 +32,12 @@ impl OptionalFmMutClosure { } /// Stores the given closure. - pub fn store(&mut self, closure:Closure) { + pub fn store(&mut self, closure:Closure) -> &Function { self.closure = Some(closure); + // TODO [mwu]: `insert` should be used when we bump rustc - and then get rid of unwrap. + // Blocked by https://github.com/enso-org/ide/issues/1028 + // The `unwrap` call is safe, because the line above set closure to `Some`. + self.js_ref().unwrap() } /// Obtain JS reference to the closure (that can be passed e.g. as a callback @@ -43,12 +47,12 @@ impl OptionalFmMutClosure { } /// Wraps given function into a Closure. - pub fn wrap(&mut self, f:impl ClosureFn) { + pub fn wrap(&mut self, f:impl ClosureFn) -> &Function { let boxed = Box::new(f); // Note: [mwu] Not sure exactly why, but compiler sometimes require this // explicit type below and sometimes does not. let wrapped:Closure = Closure::wrap(boxed); - self.store(wrapped); + self.store(wrapped) } /// Clears the current closure. @@ -57,4 +61,20 @@ impl OptionalFmMutClosure { pub fn clear(&mut self) { self.closure = None; } + + /// Register this closure as an event handler. + /// No action is taken if there is no closure stored. + pub fn add_listener(&self, target:&EventType::Target) { + if let Some(function) = self.js_ref() { + EventType::add_listener(target, function) + } + } + + /// Unregister this closure as an event handler. The closure must be the same as when it was + /// registered. + pub fn remove_listener(&self, target:&EventType::Target) { + if let Some(function) = self.js_ref() { + EventType::remove_listener(target, function) + } + } } diff --git a/src/rust/lib/system/web/src/event.rs b/src/rust/lib/system/web/src/event.rs new file mode 100644 index 0000000000..8263da9c39 --- /dev/null +++ b/src/rust/lib/system/web/src/event.rs @@ -0,0 +1,51 @@ +//! Utilities for DOM events. + +pub mod listener; + +use crate::prelude::*; + +use js_sys::Function; +use web_sys::EventTarget; + + + +// ============= +// === Event === +// ============= + +/// This trait represents a type of event that may fire from some specific JS `EventTarget`. +/// +/// For example, `WebSocket.close` is such an event, where `close` is event type and `WebSocket` is +/// the `EventTarget`. +/// +/// The purpose is to increase type safety by grouping event type name, event target type and event +/// value type together. +/// +/// Typically this trait is to be implemented for uncreatable types, created for the sole +/// purpose of denoting a particular event type within a context of an event target. +pub trait Type { + /// The event value -- i.e. the Rust type of a value that will be passed as an argument + /// to the listener. + /// For example `web_sys::CloseEvent`. + type Interface : AsRef; + + /// The type of the EventTarget object that fires this type of event, e.g. `web_sys::WebSocket`. + type Target : AsRef + AsRef + Clone + PartialEq; + + /// The type of the event as a string. For example `"close"`. + const NAME:&'static str; + + /// Add a given function to the event's target as an event listener. It will be called each + /// time event fires until listener is removed through `remove_listener`. + fn add_listener(target:&Self::Target, listener:&Function) { + // The unwrap here is safe, as the `addEventListener` never throws. + EventTarget::add_event_listener_with_callback(target.as_ref(), Self::NAME, listener).unwrap() + } + + /// Remove the event listener. The `add_listener` method should have been called before with + /// the very same function argument. + fn remove_listener(target:&Self::Target, listener:&Function) { + // The unwrap here is safe, as the `addEventListener` never throws. + EventTarget::remove_event_listener_with_callback(target.as_ref(), Self::NAME, listener).unwrap() + } +} diff --git a/src/rust/lib/system/web/src/event/listener.rs b/src/rust/lib/system/web/src/event/listener.rs new file mode 100644 index 0000000000..1d0db538e9 --- /dev/null +++ b/src/rust/lib/system/web/src/event/listener.rs @@ -0,0 +1,117 @@ +use crate::prelude::*; + +use crate::closure::storage::ClosureFn; +use crate::closure::storage::OptionalFmMutClosure; + + + +// ============ +// === Slot === +// ============ + +/// A Slot stores a callback and manages its connection with JS `EventTarget`. +/// +/// Both callback and the target can be set independently using `set_target` and `set_callback`. +/// Additionally, callback can be cleared at any point by `clear_callback`. +/// +/// When both target and callback are set, slot ensures that the callback is registered as an +/// event listener in the target. +/// +/// When changing target, `Slot` reattaches callback. +/// +/// `Slot` owns callback and wraps it into JS closure. `Slot` also keeps reference to the target, +/// so it must not be leaked. +#[derive(Derivative)] +#[derivative(Debug(bound="EventType::Interface: Debug"))] +pub struct Slot { + logger : Logger, + #[derivative(Debug="ignore")] + target : Option, + js_closure : OptionalFmMutClosure, +} + +impl Slot { + /// Create a new `Slot`. As the initial target is provided, the listener will register once it + /// gets a callback (see [[set_callback]]). + pub fn new(target:&EventType::Target, logger:impl AnyLogger) -> Self { + Self { + logger : Logger::sub(logger, EventType::NAME), + target : Some(target.clone()), + js_closure : default(), + } + } + + /// Register the event listener if both target and callback are set. + fn add_if_active(&mut self) { + if let (Some(target), Some(function)) = (self.target.as_ref(), self.js_closure.js_ref()) { + debug!(self.logger,"Attaching the callback."); + EventType::add_listener(target, function) + } + } + + /// Unregister the event listener if both target and callback are set. + fn remove_if_active(&mut self) { + if let (Some(target), Some(function)) = (self.target.as_ref(), self.js_closure.js_ref()) { + debug!(self.logger,"Detaching the callback."); + EventType::remove_listener(target, function) + } + } + + /// Set a new target. + /// + /// If callback is set, it will be reattached as a listener to a newly set target. + pub fn set_target(&mut self, target:&EventType::Target) { + // Prevent spurious reattaching that could affect listeners order. + if Some(target) != self.target.as_ref() { + self.remove_if_active(); + self.target = Some(target.clone()); + self.add_if_active() + } + } + + /// Clear event target. + /// + /// If callback is set, it will be unregistered. + pub fn clear_target(&mut self, target:&EventType::Target) { + // Prevent spurious reattaching that could affect listeners order. + if Some(target) != self.target.as_ref() { + self.remove_if_active(); + self.target = None; + } + } + + /// Assign a new event callback closure and register it in the target. + /// + /// If the listener was registered with the previous closure, it will unregister first. + /// + /// Caveat: using this method will move the event listener to the end of the registered + /// callbacks. This will affect the order of callback calls. + pub fn set_callback(&mut self, f:impl ClosureFn) { + self.remove_if_active(); + self.js_closure.wrap(f); + self.add_if_active() + } + + /// Erase the callback. + /// + /// The stored closure will be dropped and event listener unregistered. + pub fn clear_callback(&mut self) { + self.remove_if_active(); + self.js_closure.clear(); + } + + /// Detach and attach the listener to the target. + /// + /// The purpose is to move this slot to the end of the listeners list. + pub fn reattach(&mut self) { + self.remove_if_active(); + self.add_if_active(); + } +} + +/// Unregister listener on drop. +impl Drop for Slot { + fn drop(&mut self) { + self.remove_if_active(); + } +} diff --git a/src/rust/lib/system/web/src/lib.rs b/src/rust/lib/system/web/src/lib.rs index 8f9e882206..2bdb1d50b2 100644 --- a/src/rust/lib/system/web/src/lib.rs +++ b/src/rust/lib/system/web/src/lib.rs @@ -7,11 +7,14 @@ pub mod clipboard; pub mod closure; +pub mod event; pub mod resize_observer; pub mod platform; /// Common types that should be visible across the whole crate. pub mod prelude { + pub use enso_logger::*; + pub use enso_logger::DefaultInfoLogger as Logger; pub use enso_prelude::*; pub use wasm_bindgen::prelude::*; } @@ -77,11 +80,15 @@ impl From for Error { #[wasm_bindgen] extern "C" { - /// Converts given `JsValue` into a `String`. Uses JS's `String` function, - /// see: https://www.w3schools.com/jsref/jsref_string.asp #[allow(unsafe_code)] #[wasm_bindgen(js_name="String")] - pub fn js_to_string(s: JsValue) -> String; + fn js_to_string_inner(s:&JsValue) -> String; +} + +/// Converts given `JsValue` into a `String`. Uses JS's `String` function, +/// see: https://www.w3schools.com/jsref/jsref_string.asp +pub fn js_to_string(s:impl AsRef) -> String { + js_to_string_inner(s.as_ref()) } @@ -651,7 +658,7 @@ pub fn reflect_get_nested_string(target:&JsValue, keys:&[&str]) -> Result Date: Mon, 10 May 2021 17:39:31 +0200 Subject: [PATCH 10/17] Bump engine version to 0.2.11 (#1541) --- .github/workflows/gui-ci.yml | 12 +++++++----- CHANGELOG.md | 3 +++ build/workflow.js | 2 +- docs/CONTRIBUTING.md | 2 +- src/js/lib/project-manager/src/build.ts | 2 +- src/rust/ide/src/ide/initializer.rs | 4 ++-- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gui-ci.yml b/.github/workflows/gui-ci.yml index 559df429ab..1306b14561 100644 --- a/.github/workflows/gui-ci.yml +++ b/.github/workflows/gui-ci.yml @@ -246,10 +246,11 @@ jobs: run: node ./run dist --skip-version-validation --target macos if: startsWith(matrix.os,'macos') if: >- - !(contains(github.event.head_commit.message,'[ci build]') || github.ref == + !(contains(github.event.pull_request.body,'[ci build]') || + contains(github.event.head_commit.message,'[ci build]') || github.ref == 'refs/heads/develop' || github.base_ref == 'unstable' || github.base_ref - == 'stable' || github.ref == 'refs/heads/unstable' || github.ref == - 'refs/heads/stable') + == 'stable' || (github.ref == 'refs/heads/unstable' || github.ref == + 'refs/heads/stable')) build: name: Build runs-on: ${{ matrix.os }} @@ -375,10 +376,11 @@ jobs: dist/client/enso-linux-${{fromJson(steps.changelog.outputs.content).version}}.AppImage.sha256 if: startsWith(matrix.os,'ubuntu') if: >- + contains(github.event.pull_request.body,'[ci build]') || contains(github.event.head_commit.message,'[ci build]') || github.ref == 'refs/heads/develop' || github.base_ref == 'unstable' || github.base_ref - == 'stable' || github.ref == 'refs/heads/unstable' || github.ref == - 'refs/heads/stable' + == 'stable' || (github.ref == 'refs/heads/unstable' || github.ref == + 'refs/heads/stable') release_to_github: name: GitHub Release runs-on: ${{ matrix.os }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 989fae8878..ce342390bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,10 +18,13 @@ #### Enso Compiler +- [Updated Enso engine to version 0.2.11][1541]. + If you're interested in the enhancements and fixes made to the Enso compiler, you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). +[1541]: https://github.com/enso-org/ide/pull/1511 [1538]: https://github.com/enso-org/ide/pull/1538
diff --git a/build/workflow.js b/build/workflow.js index abe32cfe86..1b183d6738 100644 --- a/build/workflow.js +++ b/build/workflow.js @@ -406,7 +406,7 @@ let releaseCondition = `github.ref == 'refs/heads/unstable' || github.ref == 're /// 2. It was a pull request to the 'unstable', or the 'stable' branch. /// 3. It was a commit to the 'develop' branch. /// Otherwise, perform a simplified (faster) build only. -let buildCondition = `contains(github.event.head_commit.message,'${FLAG_FORCE_CI_BUILD}') || github.ref == 'refs/heads/develop' || github.base_ref == 'unstable' || github.base_ref == 'stable' || ${releaseCondition}` +let buildCondition = `contains(github.event.pull_request.body,'${FLAG_FORCE_CI_BUILD}') || contains(github.event.head_commit.message,'${FLAG_FORCE_CI_BUILD}') || github.ref == 'refs/heads/develop' || github.base_ref == 'unstable' || github.base_ref == 'stable' || (${releaseCondition})` let workflow = { name : "GUI CI", diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 23d8957807..255242300d 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -259,7 +259,7 @@ The following branches are used to develop the product: By default, CI would not build artifacts from `wip` and `develop` branches in order to save time and resources. If you want the artifacts to be build for your -commit, simply add `[ci build]` anywhere in your commit message. +PR, simply add `[ci build]` anywhere in the PR description. ### Skipping CHANGELOG.md change assertions diff --git a/src/js/lib/project-manager/src/build.ts b/src/js/lib/project-manager/src/build.ts index 167bbb2eb6..5797085f23 100644 --- a/src/js/lib/project-manager/src/build.ts +++ b/src/js/lib/project-manager/src/build.ts @@ -38,7 +38,7 @@ async function get_project_manager_url(): Promise { console.log('webpack target ' + target_platform) // Usually it is a good idea to synchronize this constant with `ENGINE_VERSION_FOR_NEW_PROJECTS` in // src/rust/ide/src/ide/initializer.rs. See also https://github.com/enso-org/ide/issues/1359 - const version = '0.2.10' + const version = '0.2.11' let base_url: string = 'https://github.com/enso-org/' base_url += 'enso/releases/download/' base_url += `enso-${version}/enso-project-manager-${version}` diff --git a/src/rust/ide/src/ide/initializer.rs b/src/rust/ide/src/ide/initializer.rs index 6ddb783c24..9077faa342 100644 --- a/src/rust/ide/src/ide/initializer.rs +++ b/src/rust/ide/src/ide/initializer.rs @@ -25,11 +25,11 @@ use ensogl::system::web::platform::Platform; // download required version of Engine. This should be handled properly when implementing // https://github.com/enso-org/ide/issues/1034 const PROJECT_MANAGER_TIMEOUT_SEC : u64 = 2 * 60 * 60; -const ENGINE_VERSION_SUPPORTED : &str = "^0.2.10"; +const ENGINE_VERSION_SUPPORTED : &str = "^0.2.11"; // Usually it is a good idea to synchronize this version with the bundled Engine version in // src/js/lib/project-manager/src/build.ts. See also https://github.com/enso-org/ide/issues/1359 -const ENGINE_VERSION_FOR_NEW_PROJECTS : &str = "0.2.10"; +const ENGINE_VERSION_FOR_NEW_PROJECTS : &str = "0.2.11"; From 001b477f186dc0c66704a0aae6274ce8c87a2c15 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Mon, 10 May 2021 19:19:39 +0200 Subject: [PATCH 11/17] New release in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce342390bd..c1d9ba5a27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Next Release +# Enso 2.0.0-alpha.5 (2021-05-14)
![New Learning Resources](/docs/assets/tags/new_learning_resources.svg) From f0e712e45c538a9a2e63b2cb9a1bdb4b64a01edb Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Mon, 10 May 2021 19:23:22 +0200 Subject: [PATCH 12/17] Correctly interpret string arguments as booleans in electron arguments. (#1539) --- .prettierrc.yaml | 2 +- CHANGELOG.md | 5 + src/js/lib/content/package.js | 4 + src/js/lib/content/src/{index.js => index.ts} | 407 +++++++++++------- src/js/lib/content/tsconfig.json | 7 + src/js/lib/content/webpack.config.js | 12 +- 6 files changed, 269 insertions(+), 168 deletions(-) rename src/js/lib/content/src/{index.js => index.ts} (52%) create mode 100644 src/js/lib/content/tsconfig.json diff --git a/.prettierrc.yaml b/.prettierrc.yaml index c0cf095109..4c6d313b53 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -1,6 +1,6 @@ overrides: - - files: "*.js" + - files: "*.[j|t]s" options: printWidth: 100 tabWidth: 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9120c369d7..80df8bc008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ you can find their release notes
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) +- [Fix some internal settings not being applied correctly in the IDE][1539]. + Some arguments were not passed correctly to the IDE leading to erroneous + behaviour in the electron app. This is now fixed. + #### Visual Environment - [Some command line arguments were not applied correctly in the IDE][1536]. @@ -59,6 +63,7 @@ you can find their release notes [1511]: https://github.com/enso-org/ide/pull/1511 [1536]: https://github.com/enso-org/ide/pull/1536 [1531]: https://github.com/enso-org/ide/pull/1531 +[1531]: https://github.com/enso-org/ide/pull/1539
diff --git a/src/js/lib/content/package.js b/src/js/lib/content/package.js index 64bb53e05d..4492acabf3 100644 --- a/src/js/lib/content/package.js +++ b/src/js/lib/content/package.js @@ -14,6 +14,10 @@ let config = { "compression-webpack-plugin": "^3.1.0", "copy-webpack-plugin": "^5.1.1", "yaml-loader": "^0.6.0", + "ts-loader": "^8.0.3", + "typescript": "^4.0.2", + "webpack": "^4.44.1", + "webpack-cli": "^3.3.12" } } diff --git a/src/js/lib/content/src/index.js b/src/js/lib/content/src/index.ts similarity index 52% rename from src/js/lib/content/src/index.js rename to src/js/lib/content/src/index.ts index bd239b9d05..ef7620e872 100644 --- a/src/js/lib/content/src/index.js +++ b/src/js/lib/content/src/index.ts @@ -2,14 +2,16 @@ /// user with a visual representation of this process (welcome screen). It also implements a view /// allowing to choose a debug rendering test from. +// @ts-ignore 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' -import cfg from '../../../config' -import assert from "assert"; - - +// @ts-ignore +import * as html_utils from 'enso-studio-common/src/html_utils' +// @ts-ignore +import * as globalConfig from '../../../../config.yaml' +// @ts-ignore +import cfg from '../../../config' +// @ts-ignore +import assert from 'assert' // ================= // === Constants === @@ -17,16 +19,31 @@ import assert from "assert"; const ALIVE_LOG_INTERVAL = 1000 * 60 - - // ================== // === Global API === // ================== -let API = {} -window[globalConfig.windowAppScopeName] = API +class ContentApi { + main: (inputConfig: any) => Promise + private logger: MixpanelLogger + + initLogging(config: Config) { + assert(typeof config.no_data_gathering == 'boolean') + if (!config.no_data_gathering) { + this.logger = new MixpanelLogger() + } + } + remoteLog(event: string, data?: any) { + if (this.logger) { + this.logger.log(event, data) + } + } +} +const API = new ContentApi() +// @ts-ignore +window[globalConfig.windowAppScopeName] = API // ======================== // === Content Download === @@ -37,27 +54,29 @@ let incorrect_mime_type_warning = ` 'application/wasm' MIME type. Falling back to 'WebAssembly.instantiate' which is slower. ` -function wasm_instantiate_streaming(resource,imports) { - return WebAssembly.instantiateStreaming(resource,imports).catch(e => { - return wasm_fetch.then(r => { - if (r.headers.get('Content-Type') != 'application/wasm') { - console.warn(`${incorrect_mime_type_warning} Original error:\n`, e) - return r.arrayBuffer() - } else { - throw("Server not configured to serve WASM with 'application/wasm' mime type.") - } - }).then(bytes => WebAssembly.instantiate(bytes,imports)) - }) +async function wasm_instantiate_streaming( + resource: Response, + imports: WebAssembly.Imports +): Promise { + try { + return WebAssembly.instantiateStreaming(resource, imports) + } catch (e) { + const r = await resource + if (r.headers.get('Content-Type') != 'application/wasm') { + console.warn(`${incorrect_mime_type_warning} Original error:\n`, e) + return r.arrayBuffer() + } else { + throw "Server not configured to serve WASM with 'application/wasm' mime type." + } + } } - /// Downloads the WASM binary and its dependencies. Displays loading progress bar unless provided /// with `{use_loader:false}` option. -async function download_content(config) { +async function download_content(config: { wasm_glue_url: RequestInfo; wasm_url: RequestInfo }) { let wasm_glue_fetch = await fetch(config.wasm_glue_url) - let wasm_fetch = await fetch(config.wasm_url) - let loader = - new loader_module.Loader([wasm_glue_fetch,wasm_fetch], config) + let wasm_fetch = await fetch(config.wasm_url) + let loader = new loader_module.Loader([wasm_glue_fetch, wasm_fetch], config) // TODO [mwu] // Progress indication for WASM loading is hereby capped at 30%. @@ -66,38 +85,38 @@ async function download_content(config) { // See https://github.com/enso-org/ide/issues/1237 for an immediate reason. // See https://github.com/enso-org/ide/issues/1105 for a broader context. loader.cap_progress_at = 0.3 - + loader.done.then(() => { console.groupEnd() - console.log("Download finished. Finishing WASM compilation.") + console.log('Download finished. Finishing WASM compilation.') }) let download_size = loader.show_total_bytes() let download_info = `Downloading WASM binary and its dependencies (${download_size}).` - let wasm_loader = html_utils.log_group_collapsed(download_info, async () => { + let wasm_loader = html_utils.log_group_collapsed(download_info, async () => { let wasm_glue_js = await wasm_glue_fetch.text() - let wasm_glue = Function("let exports = {};" + wasm_glue_js + "; return exports")() - let imports = wasm_glue.wasm_imports() - console.log("WASM dependencies loaded.") - console.log("Starting online WASM compilation.") - let wasm_loader = await wasm_instantiate_streaming(wasm_fetch,imports) + let wasm_glue = Function('let exports = {};' + wasm_glue_js + '; return exports')() + let imports = wasm_glue.wasm_imports() + console.log('WASM dependencies loaded.') + console.log('Starting online WASM compilation.') + let wasm_loader = await wasm_instantiate_streaming(wasm_fetch, imports) + // @ts-ignore wasm_loader.wasm_glue = wasm_glue return wasm_loader }) - let wasm = await wasm_loader.then(({instance,module,wasm_glue}) => { + // @ts-ignore + let wasm = await wasm_loader.then(({ instance, module, wasm_glue }) => { let wasm = instance.exports - wasm_glue.after_load(wasm,module) + wasm_glue.after_load(wasm, module) return wasm }) - console.log("WASM Compiled.") + console.log('WASM Compiled.') await loader.initialized - return {wasm,loader} + return { wasm, loader } } - - // ==================== // === Debug Screen === // ==================== @@ -106,46 +125,44 @@ async function download_content(config) { let main_entry_point = 'ide' /// Prefix name of each scene defined in the WASM binary. -let wasm_entry_point_pfx = "entry_point_" - +let wasm_entry_point_pfx = 'entry_point_' /// Displays a debug screen which allows the user to run one of predefined debug examples. -function show_debug_screen(wasm,msg) { - API.remoteLog("show_debug_screen") +function show_debug_screen(wasm: any, msg: string) { + API.remoteLog('show_debug_screen') let names = [] for (let fn of Object.getOwnPropertyNames(wasm)) { if (fn.startsWith(wasm_entry_point_pfx)) { - let name = fn.replace(wasm_entry_point_pfx,"") + let name = fn.replace(wasm_entry_point_pfx, '') names.push(name) } } - if(msg==="" || msg===null || msg===undefined) { msg = "" } + if (msg === '' || msg === null || msg === undefined) { + msg = '' + } let debug_screen_div = html_utils.new_top_level_div() - let newDiv = document.createElement("div") - let newContent = document.createTextNode(msg + "Available entry points:") - let currentDiv = document.getElementById("app") - let ul = document.createElement('ul') + let newDiv = document.createElement('div') + let newContent = document.createTextNode(msg + 'Available entry points:') + let ul = document.createElement('ul') debug_screen_div.style.position = 'absolute' - debug_screen_div.style.zIndex = 1 + debug_screen_div.style.zIndex = 1 newDiv.appendChild(newContent) debug_screen_div.appendChild(newDiv) newDiv.appendChild(ul) for (let name of names) { - let li = document.createElement('li') - let a = document.createElement('a') + let li = document.createElement('li') + let a = document.createElement('a') let linkText = document.createTextNode(name) ul.appendChild(li) a.appendChild(linkText) - a.title = name - a.href = "?entry="+name + a.title = name + a.href = '?entry=' + name li.appendChild(a) } } - - // ==================== // === Scam Warning === // ==================== @@ -159,52 +176,58 @@ function printScamWarning() { font-weight : bold; padding: 10px 20px 10px 20px; ` - let headerCSS1 = headerCSS + "font-size : 46px;" - let headerCSS2 = headerCSS + "font-size : 20px;" - let msgCSS = "font-size:16px;" - - let msg1 = "This is a browser feature intended for developers. If someone told you to " + - "copy-paste something here, it is a scam and will give them access to your " + - "account and data." - let msg2 = "See https://github.com/enso-org/ide/blob/main/docs/security/selfxss.md for more " + - "information." - console.log("%cStop!",headerCSS1) - console.log("%cYou may be victim of a scam!",headerCSS2) - console.log("%c"+msg1,msgCSS) - console.log("%c"+msg2,msgCSS) + let headerCSS1 = headerCSS + 'font-size : 46px;' + let headerCSS2 = headerCSS + 'font-size : 20px;' + let msgCSS = 'font-size:16px;' + + let msg1 = + 'This is a browser feature intended for developers. If someone told you to ' + + 'copy-paste something here, it is a scam and will give them access to your ' + + 'account and data.' + let msg2 = + 'See https://github.com/enso-org/ide/blob/main/docs/security/selfxss.md for more ' + + 'information.' + console.log('%cStop!', headerCSS1) + console.log('%cYou may be victim of a scam!', headerCSS2) + console.log('%c' + msg1, msgCSS) + console.log('%c' + msg2, msgCSS) } - - // ====================== // === Remote Logging === // ====================== class MixpanelLogger { + private readonly mixpanel: any + constructor() { - this.mixpanel = require('mixpanel-browser'); - this.mixpanel.init("5b541aeab5e08f313cdc1d1bbebc12ac", { "api_host": "https://api-eu.mixpanel.com" }, ""); + this.mixpanel = require('mixpanel-browser') + this.mixpanel.init( + '5b541aeab5e08f313cdc1d1bbebc12ac', + { api_host: 'https://api-eu.mixpanel.com' }, + '' + ) } - log(event,data) { + log(event: string, data: any) { if (this.mixpanel) { event = MixpanelLogger.trim_message(event) if (data !== undefined && data !== null) { data = MixpanelLogger.trim_message(JSON.stringify(data)) - this.mixpanel.track(event,{data}); + this.mixpanel.track(event, { data }) } else { - this.mixpanel.track(event); + this.mixpanel.track(event) } } else { console.warn(`Failed to log the event '${event}'.`) } } - static trim_message(message) { - const MAX_MESSAGE_LENGTH = 500; - let trimmed = message.substr(0,MAX_MESSAGE_LENGTH) + static trim_message(message: string) { + const MAX_MESSAGE_LENGTH = 500 + let trimmed = message.substr(0, MAX_MESSAGE_LENGTH) if (trimmed.length < message.length) { - trimmed += "..." + trimmed += '...' } return trimmed } @@ -214,36 +237,46 @@ class MixpanelLogger { // === Logs Buffering === // ====================== -const logsFns = ['log','info','debug','warn','error','group','groupCollapsed','groupEnd'] +const logsFns = ['log', 'info', 'debug', 'warn', 'error', 'group', 'groupCollapsed', 'groupEnd'] class LogRouter { + private buffer: any[] + private readonly raw: {} + autoFlush: boolean + constructor() { - this.buffer = [] - this.raw = {} + this.buffer = [] + this.raw = {} this.autoFlush = true + // @ts-ignore console.autoFlush = true for (let name of logsFns) { + // @ts-ignore this.raw[name] = console[name] + // @ts-ignore console[name] = (...args) => { - this.handle(name,args) + this.handle(name, args) } } } auto_flush_on() { this.autoFlush = true + // @ts-ignore console.autoFlush = true - for (let {name,args} of this.buffer) { + for (let { name, args } of this.buffer) { + // @ts-ignore this.raw[name](...args) } this.buffer = [] } - handle(name,args) { + handle(name: string, args: any[]) { if (this.autoFlush) { + // @ts-ignore this.raw[name](...args) } else { - this.buffer.push({name,args}) + this.buffer.push({ name, args }) } // The following code is just a hack to discover if the logs start with `[E]` which @@ -271,8 +304,8 @@ class LogRouter { } } - handleError(...args) { - API.remoteLog("error", args) + handleError(...args: any[]) { + API.remoteLog('error', args) } } @@ -281,6 +314,7 @@ let logRouter = new LogRouter() function hideLogs() { console.log('All subsequent logs will be hidden. Eval `showLogs()` to reveal them.') logRouter.autoFlush = false + // @ts-ignore console.autoFlush = false } @@ -288,10 +322,9 @@ function showLogs() { logRouter.auto_flush_on() } +// @ts-ignore window.showLogs = showLogs - - // ====================== // === Crash Handling === // ====================== @@ -304,7 +337,7 @@ function initCrashHandling() { } } -const crashMessageStorageKey = "crash-message" +const crashMessageStorageKey = 'crash-message' function previousCrashMessageExists() { return sessionStorage.getItem(crashMessageStorageKey) !== null @@ -314,7 +347,7 @@ function getPreviousCrashMessage() { return sessionStorage.getItem(crashMessageStorageKey) } -function storeLastCrashMessage(message) { +function storeLastCrashMessage(message: string) { sessionStorage.setItem(crashMessageStorageKey, message) } @@ -322,7 +355,6 @@ function clearPreviousCrashMessage() { sessionStorage.removeItem(crashMessageStorageKey) } - // === Crash detection === function setupCrashDetection() { @@ -341,38 +373,42 @@ function setupCrashDetection() { window.addEventListener('unhandledrejection', function (event) { // As above, we prefer stack traces. // But here, `event.reason` is not even guaranteed to be an `Error`. - handleCrash(event.reason.stack || event.reason.message || "Unhandled rejection") + handleCrash(event.reason.stack || event.reason.message || 'Unhandled rejection') }) } -function handleCrash(message) { - API.remoteLog("crash", message) +function handleCrash(message: string) { + API.remoteLog('crash', message) if (document.getElementById(crashBannerId) === null) { storeLastCrashMessage(message) location.reload() } else { - for (let element of [... document.body.childNodes]) { + // @ts-ignore + for (let element of [...document.body.childNodes]) { + // @ts-ignore if (element.id !== crashBannerId) { element.remove() } } - document.getElementById(crashBannerContentId).insertAdjacentHTML("beforeend", + document.getElementById(crashBannerContentId).insertAdjacentHTML( + 'beforeend', `
-
A second error occurred. This time, the IDE will not automatically restart.
`) +
A second error occurred. This time, the IDE will not automatically restart.
` + ) } } - // === Crash recovery === // Those IDs should be the same that are used in index.html. -const crashBannerId = "crash-banner" -const crashBannerContentId = "crash-banner-content" -const crashReportButtonId = "crash-report-button" -const crashBannerCloseButtonId = "crash-banner-close-button" - -function showCrashBanner(message) { - document.body.insertAdjacentHTML('afterbegin', +const crashBannerId = 'crash-banner' +const crashBannerContentId = 'crash-banner-content' +const crashReportButtonId = 'crash-report-button' +const crashBannerCloseButtonId = 'crash-banner-close-button' + +function showCrashBanner(message: string) { + document.body.insertAdjacentHTML( + 'afterbegin', `
@@ -390,9 +426,9 @@ function showCrashBanner(message) { report_button.onclick = async _event => { try { await reportCrash(message) - content.textContent = "Thank you, the crash was reported." + content.textContent = 'Thank you, the crash was reported.' } catch (e) { - content.textContent = "The crash could not be reported." + content.textContent = 'The crash could not be reported.' } } close_button.onclick = () => { @@ -400,20 +436,19 @@ function showCrashBanner(message) { } } -async function reportCrash(message) { +async function reportCrash(message: string) { + // @ts-ignore const crashReportHost = API[globalConfig.windowAppScopeConfigName].crash_report_host await fetch(`http://${crashReportHost}/`, { method: 'POST', mode: 'no-cors', headers: { - 'Content-Type': 'text/plain' + 'Content-Type': 'text/plain', }, - body: message - }) + body: message, + }) } - - // ======================== // === Main Entry Point === // ======================== @@ -426,6 +461,7 @@ function style_root() { /// Waits for the window to finish its show animation. It is used when the website is run in /// Electron. Please note that it returns immediately in the web browser. async function windowShowAnimation() { + // @ts-ignore await window.showAnimation } @@ -435,63 +471,104 @@ function disableContextMenu() { }) } -function ok(value) { +function ok(value: any) { return value !== null && value !== undefined } +class Config { + public use_loader: boolean + public wasm_url: string + public wasm_glue_url: string + public crash_report_host: string + public no_data_gathering: boolean + public is_in_cloud: boolean + public entry: string + + static default() { + let config = new Config() + config.use_loader = true + config.wasm_url = '/assets/ide.wasm' + config.wasm_glue_url = '/assets/wasm_imports.js' + config.crash_report_host = cfg.defaultLogServerHost + config.no_data_gathering = false + config.is_in_cloud = false + config.entry = null + return config + } + + updateFromObject(other: any) { + if (!ok(other)) { + return + } + this.use_loader = ok(other.use_loader) ? tryAsBoolean(other.use_loader) : this.use_loader + this.no_data_gathering = ok(other.no_data_gathering) + ? tryAsBoolean(other.no_data_gathering) + : this.no_data_gathering + this.is_in_cloud = ok(other.is_in_cloud) + ? tryAsBoolean(other.is_in_cloud) + : this.is_in_cloud + this.wasm_url = ok(other.wasm_url) ? tryAsString(other.wasm_url) : this.wasm_url + this.wasm_glue_url = ok(other.wasm_glue_url) + ? tryAsString(other.wasm_glue_url) + : this.wasm_glue_url + this.crash_report_host = ok(other.crash_report_host) + ? tryAsString(other.crash_report_host) + : this.crash_report_host + this.entry = ok(other.entry) ? tryAsString(other.entry) : this.entry + } +} + /// Check whether the value is a string with value `"true"`/`"false"`, if so, return the // appropriate boolean instead. Otherwise, return the original value. -function parseBooleanOrLeaveAsIs(value) { - if (value === "true"){ +function parseBooleanOrLeaveAsIs(value: any): any { + if (value === 'true') { return true } - if (value === "false"){ + if (value === 'false') { return false } return value } -/// Turn all values that have a boolean in string representation (`"true"`/`"false"`) into actual -/// booleans (`true/`false``). -function parseAllBooleans(config) { - for (const key in config) { - config[key] = parseBooleanOrLeaveAsIs(config[key]) - } +function tryAsBoolean(value: any): boolean { + value = parseBooleanOrLeaveAsIs(value) + assert(typeof value == 'boolean') + return value } -function initLogging(config) { - assert(typeof config.no_data_gathering == "boolean") - if (config.no_data_gathering ) { - API.remoteLog = function (_event, _data) {} - } else { - let logger = new MixpanelLogger - API.remoteLog = function (event,data) {logger.log(event,data)} - } +function tryAsString(value: any): string { + return value.toString() } /// Main entry point. Loads WASM, initializes it, chooses the scene to run. -API.main = async function (inputConfig) { - let defaultConfig = { - use_loader : true, - wasm_url : '/assets/ide.wasm', - wasm_glue_url : '/assets/wasm_imports.js', - crash_report_host : cfg.defaultLogServerHost, - no_data_gathering : false, - is_in_cloud : false, - } - let urlParams = new URLSearchParams(window.location.search); - let urlConfig = Object.fromEntries(urlParams.entries()) - let config = Object.assign(defaultConfig,inputConfig,urlConfig) - parseAllBooleans(config) +API.main = async function (inputConfig: any) { + const urlParams = new URLSearchParams(window.location.search) + // @ts-ignore + const urlConfig = Object.fromEntries(urlParams.entries()) + + const config = Config.default() + config.updateFromObject(inputConfig) + config.updateFromObject(urlConfig) + + // @ts-ignore API[globalConfig.windowAppScopeConfigName] = config - initLogging(config) + API.initLogging(config) + + window.setInterval(() => { + API.remoteLog('alive') + }, ALIVE_LOG_INTERVAL) - window.setInterval(() =>{API.remoteLog("alive");}, ALIVE_LOG_INTERVAL) - //Build data injected during the build process. See `webpack.config.js` for the source. - API.remoteLog("git_hash", {hash: GIT_HASH}) - API.remoteLog("build_information", BUILD_INFO) - API.remoteLog("git_status", {satus: GIT_STATUS}) + // Build data injected during the build process. See `webpack.config.js` for the source. + // @ts-ignore + const hash = GIT_HASH + API.remoteLog('git_hash', { hash }) + // @ts-ignore + const buildInfo = BUILD_INFO + API.remoteLog('build_information', buildInfo) + // @ts-ignore + const status = GIT_STATUS + API.remoteLog('git_status', { status }) //initCrashHandling() style_root() @@ -500,21 +577,23 @@ API.main = async function (inputConfig) { disableContextMenu() let entryTarget = ok(config.entry) ? config.entry : main_entry_point - config.use_loader = config.use_loader && (entryTarget === main_entry_point) + config.use_loader = config.use_loader && entryTarget === main_entry_point - API.remoteLog("window_show_animation") + API.remoteLog('window_show_animation') await windowShowAnimation() - API.remoteLog("download_content") - let {wasm,loader} = await download_content(config) - API.remoteLog("wasm_loaded") + API.remoteLog('download_content') + let { wasm, loader } = await download_content(config) + API.remoteLog('wasm_loaded') if (entryTarget) { let fn_name = wasm_entry_point_pfx + entryTarget - let fn = wasm[fn_name] - if (fn) { fn() } else { + let fn = wasm[fn_name] + if (fn) { + fn() + } else { loader.destroy() - show_debug_screen(wasm,"Unknown entry point '" + entryTarget + "'. ") + show_debug_screen(wasm, "Unknown entry point '" + entryTarget + "'. ") } } else { - show_debug_screen(wasm) + show_debug_screen(wasm, '') } } diff --git a/src/js/lib/content/tsconfig.json b/src/js/lib/content/tsconfig.json new file mode 100644 index 0000000000..4d28ded9be --- /dev/null +++ b/src/js/lib/content/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "target": "ES5", + "module": "ES2015" + } +} \ No newline at end of file diff --git a/src/js/lib/content/webpack.config.js b/src/js/lib/content/webpack.config.js index 1f5d9c5945..44b5c1c967 100644 --- a/src/js/lib/content/webpack.config.js +++ b/src/js/lib/content/webpack.config.js @@ -18,7 +18,7 @@ const BUILD_INFO = JSON.parse(require('fs').readFileSync(buildPath, 'utf8')); module.exports = { entry: { - index: path.resolve(thisPath,'src','index.js'), + index: path.resolve(thisPath,'src','index.ts'), wasm_imports: './src/wasm_imports.js', }, output: { @@ -51,8 +51,9 @@ module.exports = { }, resolve: { alias: { - wasm_rust_glue$: path.resolve(wasmPath,'ide.js') - } + wasm_rust_glue$: path.resolve(wasmPath,'ide.js'), + }, + extensions: [ '.ts', '.js' ], }, performance: { hints: false, @@ -65,6 +66,11 @@ module.exports = { test: /\.ya?ml$/, type: 'json', use: 'yaml-loader' + }, + { + test: /\.tsx?/, + use: 'ts-loader', + exclude: /node_modules/, } ] } From 72349b8371ec511cb3708b991cc1a73928da00b5 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 11 May 2021 11:33:20 +0200 Subject: [PATCH 13/17] Correctly handle case when wasm streaming is failing. (#1561) --- CHANGELOG.md | 4 ++++ src/js/lib/content/src/index.ts | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80df8bc008..67ff060736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ #### Visual Environment - [Delete key will delete selected nodes][1538]. +- [Fixed an internal error that would make the IDE fail on some browser.][1561]. + Instead of crashing on browser that don't support the feature we use, we are + now just start a little bit slower. #### EnsoGL (rendering engine) @@ -24,6 +27,7 @@ you can find their release notes [1541]: https://github.com/enso-org/ide/pull/1511 [1538]: https://github.com/enso-org/ide/pull/1538 +[1561]: https://github.com/enso-org/ide/pull/1561
diff --git a/src/js/lib/content/src/index.ts b/src/js/lib/content/src/index.ts index ef7620e872..58e8dd89bf 100644 --- a/src/js/lib/content/src/index.ts +++ b/src/js/lib/content/src/index.ts @@ -57,14 +57,14 @@ let incorrect_mime_type_warning = ` async function wasm_instantiate_streaming( resource: Response, imports: WebAssembly.Imports -): Promise { +): Promise { try { return WebAssembly.instantiateStreaming(resource, imports) } catch (e) { - const r = await resource - if (r.headers.get('Content-Type') != 'application/wasm') { + if (resource.headers.get('Content-Type') !== 'application/wasm') { console.warn(`${incorrect_mime_type_warning} Original error:\n`, e) - return r.arrayBuffer() + const buffer = await resource.arrayBuffer() + return WebAssembly.instantiate(buffer,imports) } else { throw "Server not configured to serve WASM with 'application/wasm' mime type." } From fa1c8cda65e72f5caacd55fdf9d9bc639b7ed190 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 11 May 2021 14:09:05 +0200 Subject: [PATCH 14/17] fix: Apply correct content type to wasm file. --- build/workflow.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build/workflow.js b/build/workflow.js index 1b183d6738..4219a227fe 100644 --- a/build/workflow.js +++ b/build/workflow.js @@ -325,9 +325,9 @@ prepareAwsSessionCDN = { } function uploadToCDN(...names) { - let actions = [] + const actions = [] for (let name of names) { - let action = { + const action = { name: `Upload '${name}' to CDN`, shell: "bash", run: `aws s3 cp ./artifacts/content/assets/${name} ` @@ -337,6 +337,9 @@ function uploadToCDN(...names) { if (name.endsWith(".gz")) { action.run += " --content-encoding gzip"; } + if (name.endsWith(".wasm")) { + action.run += " --content-type 'application/wasm'"; + } actions.push(action) } return actions From 81410437d0b264a55ca7130ddf069a3a62bef4d8 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Tue, 11 May 2021 14:33:19 +0200 Subject: [PATCH 15/17] Revert "fix: Apply correct content type to wasm file." This reverts commit fa1c8cda --- build/workflow.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/build/workflow.js b/build/workflow.js index 4219a227fe..1b183d6738 100644 --- a/build/workflow.js +++ b/build/workflow.js @@ -325,9 +325,9 @@ prepareAwsSessionCDN = { } function uploadToCDN(...names) { - const actions = [] + let actions = [] for (let name of names) { - const action = { + let action = { name: `Upload '${name}' to CDN`, shell: "bash", run: `aws s3 cp ./artifacts/content/assets/${name} ` @@ -337,9 +337,6 @@ function uploadToCDN(...names) { if (name.endsWith(".gz")) { action.run += " --content-encoding gzip"; } - if (name.endsWith(".wasm")) { - action.run += " --content-type 'application/wasm'"; - } actions.push(action) } return actions From 9d8c2fd1c1432418815ee639d82fd8d977f8daa3 Mon Sep 17 00:00:00 2001 From: Michael Mauderer Date: Thu, 13 May 2021 08:30:19 +0200 Subject: [PATCH 16/17] Apply correct content type to wasm file dring AWS upload. (#1563) --- .github/workflows/gui-ci.yml | 4 +++- build/workflow.js | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gui-ci.yml b/.github/workflows/gui-ci.yml index 1306b14561..3447401410 100644 --- a/.github/workflows/gui-ci.yml +++ b/.github/workflows/gui-ci.yml @@ -78,6 +78,7 @@ jobs: run: >- if [[ ${{ contains(steps.changed_files.outputs.list,'CHANGELOG.md') || contains(github.event.head_commit.message,'[ci no changelog needed]') + || contains(github.event.pull_request.body,'[ci no changelog needed]') }} == false ]]; then exit 1; fi if: >- github.base_ref == 'develop' || github.base_ref == 'unstable' || @@ -486,7 +487,8 @@ jobs: run: >- aws s3 cp ./artifacts/content/assets/ide.wasm s3://ensocdn/ide/${{fromJson(steps.changelog.outputs.content).version}}/ide.wasm - --profile s3-upload --acl public-read + --profile s3-upload --acl public-read --content-type + 'application/wasm' - name: Upload 'wasm_imports.js.gz' to CDN shell: bash run: >- diff --git a/build/workflow.js b/build/workflow.js index 1b183d6738..00c172be6b 100644 --- a/build/workflow.js +++ b/build/workflow.js @@ -273,7 +273,7 @@ let assertChangelogWasUpdated = [ getListOfChangedFiles, { name: 'Assert if CHANGELOG.md was updated (on pull request)', - run: `if [[ \${{ contains(steps.changed_files.outputs.list,'CHANGELOG.md') || contains(github.event.head_commit.message,'${FLAG_NO_CHANGELOG_NEEDED}') }} == false ]]; then exit 1; fi`, + run: `if [[ \${{ contains(steps.changed_files.outputs.list,'CHANGELOG.md') || contains(github.event.head_commit.message,'${FLAG_NO_CHANGELOG_NEEDED}') || contains(github.event.pull_request.body,'${FLAG_NO_CHANGELOG_NEEDED}') }} == false ]]; then exit 1; fi`, if: `github.base_ref == 'develop' || github.base_ref == 'unstable' || github.base_ref == 'stable'` } ] @@ -325,9 +325,9 @@ prepareAwsSessionCDN = { } function uploadToCDN(...names) { - let actions = [] + const actions = [] for (let name of names) { - let action = { + const action = { name: `Upload '${name}' to CDN`, shell: "bash", run: `aws s3 cp ./artifacts/content/assets/${name} ` @@ -337,6 +337,9 @@ function uploadToCDN(...names) { if (name.endsWith(".gz")) { action.run += " --content-encoding gzip"; } + if (name.endsWith(".wasm")) { + action.run += " --content-type 'application/wasm'"; + } actions.push(action) } return actions From f9778d60d2e34ab7b975885e1224c5b8b3b2edc3 Mon Sep 17 00:00:00 2001 From: Felix Rech Date: Thu, 13 May 2021 08:50:01 +0100 Subject: [PATCH 17/17] Deselect visualization when its node gets removed (#1556) --- CHANGELOG.md | 4 ++++ src/rust/ide/view/graph-editor/src/lib.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ff060736..cc10b42221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ #### Visual Environment - [Delete key will delete selected nodes][1538]. +- [It is possible to move around after deleting a node with a selected + visualization][1556]. Deleting a node while its attached visualization was + selected made it impossible to pan or zoom around the stage afterwards. This + error is fixed now. - [Fixed an internal error that would make the IDE fail on some browser.][1561]. Instead of crashing on browser that don't support the feature we use, we are now just start a little bit slower. diff --git a/src/rust/ide/view/graph-editor/src/lib.rs b/src/rust/ide/view/graph-editor/src/lib.rs index 2652336702..44c8362f9b 100644 --- a/src/rust/ide/view/graph-editor/src/lib.rs +++ b/src/rust/ide/view/graph-editor/src/lib.rs @@ -2987,6 +2987,7 @@ fn new_graph_editor(app:&Application) -> GraphEditor { eval out.node_selected ((id) model.select_node(id)); eval out.node_deselected ((id) model.deselect_node(id)); eval out.node_removed ((id) model.remove_node(id)); + out.source.on_visualization_select <+ out.node_removed.map(|&id| Switch::Off(id)); eval inputs.set_node_expression (((id,expr)) model.set_node_expression(id,expr)); port_to_refresh <= inputs.set_node_expression.map(f!(((id,_))model.node_in_edges(id)));