diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c9ec402f119..d6bd523f909f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,11 +146,19 @@ - [Fixed text visualisations which were being cut off at the last line.][6421] - [Fixed a bug where, when scrolling or dragging on a full-screen visualization, the view of the graph changed as well.][6530] +- [Changed the shortcut for restoring to the last saved version of a project + from cmd+r to + cmd+shift+r][6620] to make it less likely + that it would be triggered by accident. As a consequence, the program + execution shortcuts changed from + cmd+shift+t/r to + cmd+alt+t/r. - [Added a button to return from an opened project back to the project dashboard.][6474] [6421]: https://github.com/enso-org/enso/pull/6421 [6530]: https://github.com/enso-org/enso/pull/6530 +[6620]: https://github.com/enso-org/enso/pull/6620 [6474]: https://github.com/enso-org/enso/pull/6474 #### EnsoGL (rendering engine) @@ -743,6 +751,7 @@ finalizers][6335] - [Warning.get_all returns only unique warnings][6372] - [Reimplement `enso_project` as a proper builtin][6352] +- [Limit number of reported warnings per value][6577] [3227]: https://github.com/enso-org/enso/pull/3227 [3248]: https://github.com/enso-org/enso/pull/3248 @@ -852,6 +861,7 @@ [6335]: https://github.com/enso-org/enso/pull/6335 [6372]: https://github.com/enso-org/enso/pull/6372 [6352]: https://github.com/enso-org/enso/pull/6352 +[6577]: https://github.com/enso-org/enso/pull/6577 # Enso 2.0.0-alpha.18 (2021-10-12) diff --git a/app/gui/docs/product/shortcuts.md b/app/gui/docs/product/shortcuts.md index e1a649759797..815a2257e19b 100644 --- a/app/gui/docs/product/shortcuts.md +++ b/app/gui/docs/product/shortcuts.md @@ -40,6 +40,7 @@ broken and require further investigation. | ctrl+` | Show Code Editor. Please note that the Code Editor implementation is in a very early stage and you should not use it. Even just openning it can cause errors in the IDE. Do not try using the graph editor while having the code editor tab openned. | | cmd+o | Open project | | cmd+s | Save module | +| cmd+shift+r | Restore module from last save | | cmd+z | Undo last action | | cmd+y or cmd + shift + z | Redo last undone action | | cmd+q | Close the application (MacOS) | @@ -48,8 +49,8 @@ broken and require further investigation. | ctrl+w | Close the application (Windows, Linux) | | :warning: ctrl+p | Toggle profiling mode | | escape | Cancel current action. For example, drop currently dragged connection. | -| cmd+shift+t | Terminate the program execution | -| cmd+shift+r | Re-execute the program | +| cmd+alt+t | Terminate the program execution | +| cmd+alt+r | Re-execute the program | | cmd+shift+k | Switch the execution environment to Design. | | cmd+shift+l | Switch the execution environment to Live. | diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index 962978bf5413..23040a069977 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -744,14 +744,14 @@ impl application::View for View { (Press, "", "cmd alt shift t", "toggle_style"), (Press, "", "cmd alt p", "toggle_component_browser_private_entries_visibility"), (Press, "", "cmd s", "save_project_snapshot"), - (Press, "", "cmd r", "restore_project_snapshot"), + (Press, "", "cmd shift r", "restore_project_snapshot"), (Press, "", "cmd z", "undo"), (Press, "", "cmd y", "redo"), (Press, "", "cmd shift z", "redo"), (Press, "!debug_mode", DEBUG_MODE_SHORTCUT, "enable_debug_mode"), (Press, "debug_mode", DEBUG_MODE_SHORTCUT, "disable_debug_mode"), - (Press, "", "cmd shift t", "execution_context_interrupt"), - (Press, "", "cmd shift r", "execution_context_restart"), + (Press, "", "cmd alt t", "execution_context_interrupt"), + (Press, "", "cmd alt r", "execution_context_restart"), // TODO(#6179): Remove this temporary shortcut when Play button is ready. (Press, "", "ctrl shift b", "toggle_read_only"), ] diff --git a/app/ide-desktop/lib/client/src/bin/server.ts b/app/ide-desktop/lib/client/src/bin/server.ts index 642c512cd12d..ed18a44305cc 100644 --- a/app/ide-desktop/lib/client/src/bin/server.ts +++ b/app/ide-desktop/lib/client/src/bin/server.ts @@ -8,8 +8,11 @@ import * as mime from 'mime-types' import * as portfinder from 'portfinder' import createServer from 'create-servers' +import * as common from 'enso-common' import * as contentConfig from 'enso-content-config' +import * as paths from '../paths' + const logger = contentConfig.logger // ================= @@ -18,20 +21,6 @@ const logger = contentConfig.logger const HTTP_STATUS_OK = 200 -// ====================== -// === URL Parameters === -// ====================== - -/** Construct URL query with the given parameters. For each `key` - `value` pair, - * `key=value` will be added to the query. */ -export function urlParamsFromObject(obj: Record) { - const params = [] - for (const [key, value] of Object.entries(obj)) { - params.push(`${key}=${encodeURIComponent(value)}`) - } - return params.length === 0 ? '' : '?' + params.join('&') -} - // ============== // === Config === // ============== @@ -110,7 +99,16 @@ export class Server { } else { const url = requestUrl.split('?')[0] const resource = url === '/' ? '/index.html' : requestUrl - const resourceFile = `${this.config.dir}${resource}` + // `preload.cjs` must be specialcased here as it is loaded by electron from the root, + // in contrast to all assets loaded by the window, which are loaded from `assets/` via + // this server. + const resourceFile = + resource === '/preload.cjs.map' + ? `${paths.APP_PATH}${resource}` + : `${this.config.dir}${resource}` + for (const [header, value] of common.COOP_COEP_CORP_HEADERS) { + response.setHeader(header, value) + } fs.readFile(resourceFile, (err, data) => { if (err) { logger.error(`Resource '${resource}' not found.`) diff --git a/app/ide-desktop/lib/client/src/index.ts b/app/ide-desktop/lib/client/src/index.ts index 4b96ca247dd3..d6be51e2e5e6 100644 --- a/app/ide-desktop/lib/client/src/index.ts +++ b/app/ide-desktop/lib/client/src/index.ts @@ -345,16 +345,17 @@ class App { /** Redirect the web view to `localhost:` to see the served website. */ loadWindowContent() { if (this.window != null) { - const urlCfg: Record = {} + const searchParams: Record = {} for (const option of this.args.optionsRecursive()) { if (option.value !== option.default && option.passToWebApplication) { - urlCfg[option.qualifiedName()] = String(option.value) + searchParams[option.qualifiedName()] = option.value.toString() } } - const params = server.urlParamsFromObject(urlCfg) - const address = `http://localhost:${this.serverPort()}${params}` - logger.log(`Loading the window address '${address}'.`) - void this.window.loadURL(address) + const address = new URL('http://localhost') + address.port = this.serverPort().toString() + address.search = new URLSearchParams(searchParams).toString() + logger.log(`Loading the window address '${address.toString()}'.`) + void this.window.loadURL(address.toString()) } } @@ -423,7 +424,7 @@ class App { // =================== process.on('uncaughtException', (err, origin) => { - console.error(`Uncaught exception: ${String(err)}\nException origin: ${origin}`) + console.error(`Uncaught exception: ${err.toString()}\nException origin: ${origin}`) electron.dialog.showErrorBox(common.PRODUCT_NAME, err.stack ?? err.toString()) electron.app.exit(1) }) diff --git a/app/ide-desktop/lib/common/src/index.ts b/app/ide-desktop/lib/common/src/index.ts index 0b514c5da626..f95ae98b0713 100644 --- a/app/ide-desktop/lib/common/src/index.ts +++ b/app/ide-desktop/lib/common/src/index.ts @@ -1,4 +1,5 @@ -/** @file This module contains metadata about the product and distribution. +/** @file This module contains metadata about the product and distribution, + * and various other constants that are needed in multiple sibling packages. * * Code in this package is used by two or more sibling packages of this package. The code is defined * here when it is not possible for a sibling package to own that code without introducing a @@ -12,3 +13,13 @@ export const DEEP_LINK_SCHEME = 'enso' /** Name of the product. */ export const PRODUCT_NAME = 'Enso' + +/** COOP, COEP, and CORP headers: https://web.dev/coop-coep/ + * + * These are required to increase the resolution of `performance.now()` timers, + * making profiling a lot more accurate and consistent. */ +export const COOP_COEP_CORP_HEADERS: [header: string, value: string][] = [ + ['Cross-Origin-Embedder-Policy', 'require-corp'], + ['Cross-Origin-Opener-Policy', 'same-origin'], + ['Cross-Origin-Resource-Policy', 'same-origin'], +] diff --git a/app/ide-desktop/lib/content/esbuild-config.ts b/app/ide-desktop/lib/content/esbuild-config.ts index f53c1002f02e..eb80ad2b9f50 100644 --- a/app/ide-desktop/lib/content/esbuild-config.ts +++ b/app/ide-desktop/lib/content/esbuild-config.ts @@ -26,7 +26,11 @@ import esbuildPluginYaml from 'esbuild-plugin-yaml' import * as utils from '../../utils' import BUILD_INFO from '../../build.json' assert { type: 'json' } -export const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url))) +// ================= +// === Constants === +// ================= + +const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url))) // ============================= // === Environment variables === @@ -88,6 +92,7 @@ export function bundlerOptions(args: Arguments) { loader: { '.html': 'copy', '.css': 'copy', + '.map': 'copy', '.wasm': 'copy', '.svg': 'copy', '.png': 'copy', diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index b3f27b68b35c..ea413eb888ff 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -20,6 +20,9 @@ const logger = app.log.logger const ESBUILD_PATH = '/esbuild' /** SSE event indicating a build has finished. */ const ESBUILD_EVENT_NAME = 'change' +/** Path to the service worker that resolves all extensionless paths to `/index.html`. + * This service worker is required for client-side routing to work when doing local development. */ +const SERVICE_WORKER_PATH = '/serviceWorker.js' /** One second in milliseconds. */ const SECOND = 1000 /** Time in seconds after which a `fetchTimeout` ends. */ @@ -33,6 +36,7 @@ if (IS_DEV_MODE) { new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => { location.reload() }) + void navigator.serviceWorker.register(SERVICE_WORKER_PATH) } // ============= diff --git a/app/ide-desktop/lib/content/src/serviceWorker.ts b/app/ide-desktop/lib/content/src/serviceWorker.ts new file mode 100644 index 000000000000..332b8849acc8 --- /dev/null +++ b/app/ide-desktop/lib/content/src/serviceWorker.ts @@ -0,0 +1,32 @@ +/** @file A service worker that redirects paths without extensions to `/index.html`. + * This is required for paths like `/login`, which are handled by client-side routing, + * to work when developing locally on `localhost:8080`. */ +// Bring globals and interfaces specific to Web Workers into scope. +/// +import * as common from 'enso-common' + +// ===================== +// === Fetch handler === +// ===================== + +// We `declare` a variable here because Service Workers have a different global scope. +// eslint-disable-next-line no-restricted-syntax +declare const self: ServiceWorkerGlobalScope + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url) + if (url.hostname === 'localhost' && url.pathname !== '/esbuild') { + event.respondWith( + fetch(event.request.url).then(response => { + const clonedResponse = new Response(response.body, response) + for (const [header, value] of common.COOP_COEP_CORP_HEADERS) { + clonedResponse.headers.set(header, value) + } + return clonedResponse + }) + ) + return + } else { + return false + } +}) diff --git a/app/ide-desktop/lib/content/watch.ts b/app/ide-desktop/lib/content/watch.ts index 44385f47760a..a35afb242d0d 100644 --- a/app/ide-desktop/lib/content/watch.ts +++ b/app/ide-desktop/lib/content/watch.ts @@ -1,4 +1,7 @@ /** @file File watch and compile service. */ +import * as path from 'node:path' +import * as url from 'node:url' + import * as esbuild from 'esbuild' import * as portfinder from 'portfinder' import chalk from 'chalk' @@ -12,6 +15,7 @@ import * as dashboardBundler from '../dashboard/esbuild-config' const PORT = 8080 const HTTP_STATUS_OK = 200 +const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url))) // =============== // === Watcher === @@ -29,6 +33,10 @@ async function watch() { ...bundler.argumentsFromEnv(), devMode: true, }) + opts.entryPoints.push({ + in: path.resolve(THIS_PATH, 'src', 'serviceWorker.ts'), + out: 'serviceWorker', + }) const builder = await esbuild.context(opts) await builder.watch() await builder.serve({ diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx index a00b416e20e9..d36b439bf229 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx @@ -254,6 +254,16 @@ export const COMPUTER_ICON = ( ) +/** An icon representing a user without a profile picture. */ +export const DEFAULT_USER_ICON = ( + + + +) + export interface StopIconProps { className?: string } diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx index 9759f73002ee..74a4ebaa8a03 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/changePasswordModal.tsx @@ -38,6 +38,10 @@ function ChangePasswordModal() { onClick={event => { event.stopPropagation() }} + onSubmit={async event => { + event.preventDefault() + await onSubmit() + }} className="flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full max-w-md" >
@@ -63,6 +67,8 @@ function ChangePasswordModal() {
{ + unsetModal() + await toast.promise(doDelete(), { + loading: `Deleting ${assetType}...`, + success: `Deleted ${assetType}.`, + error: `Could not delete ${assetType}.`, + }) + onSuccess() + } + return (
{ event.stopPropagation() }} + onSubmit={async event => { + event.preventDefault() + // Consider not calling `onSubmit()` here to make it harder to accidentally + // delete an important asset. + await onSubmit() + }} + className="relative bg-white shadow-soft rounded-lg w-96 p-2" > What do you want to rename the {assetType} '{name}' to?
-
-
{ - if (newName == null) { - toast.error('Please provide a new name.') - } else { - unsetModal() - await toast.promise(doRename(newName), { - loading: `Deleting ${assetType}...`, - success: `Deleted ${assetType}.`, - error: `Could not delete ${assetType}.`, - }) - onSuccess() - } - }} > Rename -
+
{/* User profile and menu. */}
- { event.stopPropagation() setUserMenuVisible(!userMenuVisible) }} - /> + className="rounded-full w-8 h-8 bg-cover cursor-pointer" + > + {svg.DEFAULT_USER_ICON} +
) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/uploadFileModal.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/uploadFileModal.tsx index 2d9d7e77728c..8a7bf254d107 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/uploadFileModal.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/uploadFileModal.tsx @@ -48,10 +48,14 @@ function UploadFileModal(props: UploadFileModalProps) { return ( { event.stopPropagation() }} + onSubmit={async event => { + event.preventDefault() + await onSubmit() + }} + className="relative bg-white shadow-soft rounded-lg w-96 h-72 p-2" >