[Gui2] Opening projects and language server connection (#7813)

# Important Notes
- Binary LS endpoint is not yet handled.
- The parsing of provided source is not entirely correct, as each line (including imports) is treated as node. The usage of actual enso AST for nodes is not yet implemented.
- Modifications to the graph state are not yet synchronized back to the language server.
This commit is contained in:
Paweł Grabarz 2023-09-22 05:43:25 +02:00 committed by GitHub
parent 3ba2f6f391
commit 42a7cb2d23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
161 changed files with 17023 additions and 42866 deletions

View File

@ -24,7 +24,7 @@ jobs:
conda-channels: anaconda, conda-forge conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent') - if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0 uses: jetli/wasm-pack-action@v0.4.0
with: with:
version: v0.10.2 version: v0.10.2
- name: Expose Artifact API and context information. - name: Expose Artifact API and context information.

3
.gitignore vendored
View File

@ -70,6 +70,7 @@ node_modules/
.enso-sources* .enso-sources*
.metals .metals
tools/performance/engine-benchmarks/generated_site tools/performance/engine-benchmarks/generated_site
*.tsbuildinfo
############################ ############################
## Rendered Documentation ## ## Rendered Documentation ##
@ -108,7 +109,7 @@ bench-report*.xml
.bloop/ .bloop/
.bsp/ .bsp/
project/metals.sbt project/metals.sbt
/app/ide-desktop/build.json /build.json
/app/ide-desktop/lib/client/electron-builder-config.json /app/ide-desktop/lib/client/electron-builder-config.json

1
.npmrc Normal file
View File

@ -0,0 +1 @@
legacy-peer-deps=true

View File

@ -26,11 +26,13 @@ test/**/data
**/msdfgen_wasm.js **/msdfgen_wasm.js
# Generated files # Generated files
app/ide-desktop/build.json
app/ide-desktop/lib/client/electron-builder-config.json app/ide-desktop/lib/client/electron-builder-config.json
app/ide-desktop/lib/content-config/src/config.json
app/gui/view/documentation/assets/stylesheet.css app/gui/view/documentation/assets/stylesheet.css
app/gui2/rust-ffi/pkg app/gui2/rust-ffi/pkg
Cargo.lock Cargo.lock
build.json
app/gui2/playwright-report/
# Engine Builds can leave these nested working copies. # Engine Builds can leave these nested working copies.
# TODO [mwu]: Adjust Engine build to not leave them. # TODO [mwu]: Adjust Engine build to not leave them.

10
.vscode/settings.json vendored
View File

@ -6,6 +6,16 @@
"auto-snippets.snippets": [ "auto-snippets.snippets": [
{ "language": "vue", "snippet": "Vue single-file component" } { "language": "vue", "snippet": "Vue single-file component" }
], ],
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.experimental.useFlatConfig": true,
"eslint.useESLintClass": true,
"[javascript][typescript][typescriptreact][vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
},
"eslint.workingDirectories": [
"./app/gui2",
"./app/gui2/ide-desktop"
],
"files.watcherExclude": { "files.watcherExclude": {
"**/target": true "**/target": true
} }

34
Cargo.lock generated
View File

@ -5995,6 +5995,16 @@ dependencies = [
"xmlparser", "xmlparser",
] ]
[[package]]
name = "rust-ffi"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"enso-parser",
"serde_json",
"wasm-bindgen",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.21" version = "0.1.21"
@ -7425,9 +7435,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.84" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -7435,16 +7445,16 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.84" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.107", "syn 2.0.15",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -7462,9 +7472,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.84" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -7472,22 +7482,22 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.84" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.107", "syn 2.0.15",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.84" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"

View File

@ -10,6 +10,7 @@ members = [
"app/gui", "app/gui",
"app/gui/language/parser", "app/gui/language/parser",
"app/gui/enso-profiler-enso-data", "app/gui/enso-profiler-enso-data",
"app/gui2/rust-ffi",
"build/cli", "build/cli",
"build/macros/proc-macro", "build/macros/proc-macro",
"build/ci-gen", "build/ci-gen",
@ -93,7 +94,7 @@ serde-wasm-bindgen = { version = "0.4.5" }
tokio = { version = "1.23.0", features = ["full", "tracing"] } tokio = { version = "1.23.0", features = ["full", "tracing"] }
tokio-stream = { version = "0.1.12", features = ["fs"] } tokio-stream = { version = "0.1.12", features = ["fs"] }
tokio-util = { version = "0.7.4", features = ["full"] } tokio-util = { version = "0.7.4", features = ["full"] }
wasm-bindgen = { version = "0.2.84", features = [] } wasm-bindgen = { version = "0.2.87", features = [] }
wasm-bindgen-test = { version = "0.3.34" } wasm-bindgen-test = { version = "0.3.34" }
anyhow = { version = "1.0.66" } anyhow = { version = "1.0.66" }
failure = { version = "0.1.8" } failure = { version = "0.1.8" }

View File

@ -60,52 +60,43 @@ impl BackendService {
/// Read backend configuration from the web arguments. See also [`web::Arguments`] /// Read backend configuration from the web arguments. See also [`web::Arguments`]
/// documentation. /// documentation.
pub fn from_web_arguments(args: &Args) -> FallibleResult<Self> { pub fn from_web_arguments(args: &Args) -> FallibleResult<Self> {
let endpoint = args.groups.engine.options.project_manager_url.value.as_str();
let rpc_url_option = &args.groups.engine.options.rpc_url; let rpc_url_option = &args.groups.engine.options.rpc_url;
let data_url_option = &args.groups.engine.options.data_url; let data_url_option = &args.groups.engine.options.data_url;
let rpc_url = rpc_url_option.value.as_str(); let rpc_url = rpc_url_option.value.as_str();
let data_url = data_url_option.value.as_str(); let data_url = data_url_option.value.as_str();
if !endpoint.is_empty() {
if !rpc_url.is_empty() || !data_url.is_empty() { match (rpc_url, data_url) {
Err(MutuallyExclusiveOptions.into()) ("", "") => Ok(default()),
} else { ("", _) => Err(MissingOption(rpc_url_option.__name__.to_owned()).into()),
let endpoint = endpoint.to_owned(); (_, "") => Err(MissingOption(data_url_option.__name__.to_owned()).into()),
Ok(Self::ProjectManager { endpoint }) (json_endpoint, binary_endpoint) => {
} let json_endpoint = json_endpoint.to_owned();
} else { let binary_endpoint = binary_endpoint.to_owned();
match (rpc_url, data_url) { let def_namespace = || constants::DEFAULT_PROJECT_NAMESPACE.to_owned();
("", "") => Ok(default()), let namespace = args.groups.engine.options.namespace.value.clone();
("", _) => Err(MissingOption(rpc_url_option.__name__.to_owned()).into()), let namespace = if namespace.is_empty() { def_namespace() } else { namespace };
(_, "") => Err(MissingOption(data_url_option.__name__.to_owned()).into()), let project_name_option = &args.groups.startup.options.project;
(json_endpoint, binary_endpoint) => { let project_name = project_name_option.value.as_str();
let json_endpoint = json_endpoint.to_owned(); let no_project_name = || MissingOption(project_name_option.__name__.to_owned());
let binary_endpoint = binary_endpoint.to_owned(); let project_name = if project_name.is_empty() {
let def_namespace = || constants::DEFAULT_PROJECT_NAMESPACE.to_owned(); Err(no_project_name())
let namespace = args.groups.engine.options.namespace.value.clone(); } else {
let namespace = if namespace.is_empty() { def_namespace() } else { namespace }; Ok(project_name.to_owned())
let project_name_option = &args.groups.startup.options.project; }?;
let project_name = project_name_option.value.as_str(); let displayed_name_option = &args.groups.startup.options.displayed_project_name;
let no_project_name = || MissingOption(project_name_option.__name__.to_owned()); let displayed_name = displayed_name_option.value.as_str();
let project_name = if project_name.is_empty() { let displayed_name = if displayed_name.is_empty() {
Err(no_project_name()) project_name.clone()
} else { } else {
Ok(project_name.to_owned()) displayed_name.to_owned()
}?; };
let displayed_name_option = &args.groups.startup.options.displayed_project_name; Ok(Self::LanguageServer {
let displayed_name = displayed_name_option.value.as_str(); json_endpoint,
let displayed_name = if displayed_name.is_empty() { binary_endpoint,
project_name.clone() namespace,
} else { project_name,
displayed_name.to_owned() displayed_name,
}; })
Ok(Self::LanguageServer {
json_endpoint,
binary_endpoint,
namespace,
project_name,
displayed_name,
})
}
} }
} }
} }

View File

@ -69,7 +69,8 @@
.enso-internal-templates-view .enso-internal-card { .enso-internal-templates-view .enso-internal-card {
border-radius: 20px; border-radius: 20px;
color: #fcfeff; color: #fcfeff;
box-shadow: 0px 36px 51px rgba(0, 0, 0, 0.03), box-shadow:
0px 36px 51px rgba(0, 0, 0, 0.03),
0px 15.0399px 21.3066px rgba(0, 0, 0, 0.0232911), 0px 15.0399px 21.3066px rgba(0, 0, 0, 0.0232911),
0px 8.04107px 11.3915px rgba(0, 0, 0, 0.0197608), 0px 8.04107px 11.3915px rgba(0, 0, 0, 0.0197608),
0px 4.50776px 6.38599px rgba(0, 0, 0, 0.0166035), 0px 4.50776px 6.38599px rgba(0, 0, 0, 0.0166035),

View File

@ -1,33 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-recommended',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
],
parserOptions: {
ecmaVersion: 'latest',
},
ignorePatterns: ['rust-ffi/pkg/**'],
rules: {
camelcase: [
1,
{
ignoreImports: true,
},
],
'no-inner-declarations': 0,
'vue/v-on-event-hyphenation': [2, 'never'],
'@typescript-eslint/no-unused-vars': [
1,
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
},
],
},
}

View File

@ -1,8 +1,10 @@
{ {
"$schema": "https://json.schemastore.org/prettierrc", "$schema": "https://json.schemastore.org/prettierrc",
"plugins": ["prettier-plugin-organize-imports"],
"semi": false, "semi": false,
"tabWidth": 2, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"printWidth": 100, "printWidth": 100,
"trailingComma": "all" "trailingComma": "all",
"organizeImportsSkipDestructiveCodeActions": true
} }

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test' import { expect, test } from '@playwright/test'
// See here how to get started: // See here how to get started:
// https://playwright.dev/docs/intro // https://playwright.dev/docs/intro

5
app/gui2/env.d.ts vendored
View File

@ -1,6 +1,3 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
module 'y-websocket' { declare const PROJECT_MANAGER_URL: string
// hack for bad module resolution
export * from 'node_modules/y-websocket/dist/src/y-websocket'
}

41
app/gui2/eslint.config.js Normal file
View File

@ -0,0 +1,41 @@
import { FlatCompat } from '@eslint/eslintrc'
import eslintJs from '@eslint/js'
import * as path from 'node:path'
import * as url from 'node:url'
const compat = new FlatCompat()
const DIR_NAME = path.dirname(url.fileURLToPath(import.meta.url))
const conf = [
{
ignores: ['rust-ffi/pkg', 'dist'],
},
...compat.extends('plugin:vue/vue3-recommended'),
eslintJs.configs.recommended,
...compat.extends('@vue/eslint-config-typescript', '@vue/eslint-config-prettier'),
{
// files: ['{**,src}/*.{vue,js,jsx,cjs,mjs,ts,tsx,cts,mts}'],
languageOptions: {
parserOptions: {
tsconfigRootDir: DIR_NAME,
ecmaVersion: 'latest',
project: ['./tsconfig.app.json', './tsconfig.node.json', './tsconfig.vitest.json'],
},
},
rules: {
camelcase: [1, { ignoreImports: true }],
'no-inner-declarations': 0,
'vue/v-on-event-hyphenation': [2, 'never'],
'@typescript-eslint/no-unused-vars': [
1,
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
},
],
},
},
]
export default conf

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<div id="enso-dashboard" class="enso-dashboard"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

5
app/gui2/node.env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
module 'tailwindcss/nesting' {
import { PluginCreator } from 'postcss'
declare const plugin: PluginCreator<unknown>
export default plugin
}

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,35 @@
{ {
"name": "enso-ide", "version": "0.1.0",
"version": "0.0.0", "name": "enso-gui2",
"private": true, "private": true,
"type": "module",
"author": {
"name": "Enso Team",
"email": "contact@enso.org"
},
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check build-only", "build": "run-p typecheck build-only",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest", "test:unit": "vitest",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"test": "vitest run",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint .",
"format": "prettier --write src/", "format": "prettier --write src/ && eslint . --fix",
"build-rust-ffi": "cd rust-ffi && wasm-pack build --release --target web", "build-rust-ffi": "cd rust-ffi && wasm-pack build --release --target web",
"preinstall": "npm run build-rust-ffi" "preinstall": "npm run build-rust-ffi"
}, },
"dependencies": { "dependencies": {
"@open-rpc/client-js": "^1.8.1",
"@vueuse/core": "^10.4.1", "@vueuse/core": "^10.4.1",
"enso-authentication": "^1.0.0",
"isomorphic-ws": "^5.0.0",
"lib0": "^0.2.83", "lib0": "^0.2.83",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"postcss-nesting": "^12.0.1", "postcss-inline-svg": "^6.0.0",
"vite-plugin-top-level-await": "^1.3.1", "sha3": "^2.1.4",
"vue": "^3.3.4", "vue": "^3.3.4",
"ws": "^8.13.0", "ws": "^8.13.0",
"y-protocols": "^1.0.5", "y-protocols": "^1.0.5",
@ -29,6 +38,8 @@
"yjs": "^13.6.7" "yjs": "^13.6.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "^8.49.0",
"@playwright/test": "^1.37.0", "@playwright/test": "^1.37.0",
"@rushstack/eslint-patch": "^1.3.2", "@rushstack/eslint-patch": "^1.3.2",
"@tsconfig/node18": "^18.2.0", "@tsconfig/node18": "^18.2.0",
@ -36,19 +47,26 @@
"@types/node": "^18.17.5", "@types/node": "^18.17.5",
"@types/shuffle-seed": "^1.1.0", "@types/shuffle-seed": "^1.1.0",
"@types/ws": "^8.5.5", "@types/ws": "^8.5.5",
"@vitejs/plugin-react": "^4.0.4",
"@vitejs/plugin-vue": "^4.3.1", "@vitejs/plugin-vue": "^4.3.1",
"@volar/vue-typescript": "^1.6.5",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "^2.4.1", "@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0", "@vue/tsconfig": "^0.4.0",
"eslint": "^8.46.0", "esbuild": "^0.19.3",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.16.1", "eslint-plugin-vue": "^9.16.1",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"npm-run-all": "^4.1.5", "postcss-nesting": "^12.0.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"prettier-plugin-organize-imports": "^3.2.3",
"shuffle-seed": "^1.1.6", "shuffle-seed": "^1.1.6",
"typescript": "~5.1.6", "tailwindcss": "^3.2.7",
"typescript": "~5.2.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-top-level-await": "^1.3.1",
"vitest": "^0.34.2", "vitest": "^0.34.2",
"vue-tsc": "^1.8.8" "vue-tsc": "^1.8.8"
} }

View File

@ -1,6 +0,0 @@
/* eslint-env node */
module.exports = {
plugins: {
autoprefixer: {},
},
}

View File

@ -1,904 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
dependencies = [
"lazy_static",
"regex",
]
[[package]]
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "approx"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278"
dependencies = [
"num-traits",
]
[[package]]
name = "assert_approx_eq"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "boolinator"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "bumpalo"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytemuck"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.31",
]
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 1.0.109",
]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "enso-data-structures"
version = "0.2.0"
dependencies = [
"bytemuck",
"enso-prelude",
"failure",
"rustversion",
"serde",
"typenum",
]
[[package]]
name = "enso-logging"
version = "0.3.1"
dependencies = [
"enso-logging-macros",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "enso-logging-macros"
version = "0.1.0"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.31",
]
[[package]]
name = "enso-macro-utils"
version = "0.2.0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enso-metamodel"
version = "0.1.0"
dependencies = [
"derivative",
"derive_more",
"enso-zst",
]
[[package]]
name = "enso-parser"
version = "0.1.0"
dependencies = [
"bincode",
"enso-data-structures",
"enso-parser-syntax-tree-visitor",
"enso-prelude",
"enso-reflect",
"enso-shapely-macros",
"enso-types",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "enso-parser-syntax-tree-visitor"
version = "0.1.0"
dependencies = [
"enso-macro-utils",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enso-prelude"
version = "0.2.6"
dependencies = [
"anyhow",
"assert_approx_eq",
"backtrace",
"boolinator",
"bytemuck",
"derivative",
"derive_more",
"enso-logging",
"enso-reflect",
"enso-shapely",
"enso-zst",
"failure",
"futures",
"gen-iter",
"itertools",
"lazy_static",
"paste",
"serde",
"serde_json",
"smallvec",
"wasm-bindgen",
"weak-table",
"web-sys",
]
[[package]]
name = "enso-reflect"
version = "0.1.0"
dependencies = [
"derivative",
"enso-metamodel",
"enso-reflect-macros",
]
[[package]]
name = "enso-reflect-macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enso-shapely"
version = "0.2.0"
dependencies = [
"derivative",
"enso-shapely-macros",
"enso-zst",
"paste",
"rustversion",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "enso-shapely-macros"
version = "0.2.1"
dependencies = [
"Inflector",
"boolinator",
"enso-macro-utils",
"itertools",
"paste",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enso-types"
version = "0.1.0"
dependencies = [
"enso-prelude",
"enso-reflect",
"nalgebra",
"num-traits",
"paste",
"serde",
]
[[package]]
name = "enso-zst"
version = "0.1.0"
dependencies = [
"bytemuck",
"paste",
"serde",
]
[[package]]
name = "failure"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
dependencies = [
"backtrace",
"failure_derive",
]
[[package]]
name = "failure_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"synstructure",
]
[[package]]
name = "futures"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
[[package]]
name = "futures-executor"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-macro"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.31",
]
[[package]]
name = "futures-sink"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
[[package]]
name = "futures-task"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
[[package]]
name = "futures-util"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gen-iter"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1668ac3c7b8cc5f1e31565ed509d8d70aa1a81bd7f508b620725b78c6e1d7049"
[[package]]
name = "gimli"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "matrixmultiply"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memchr"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "nalgebra"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "476d1d59fe02fe54c86356e91650cd892f392782a1cb9fc524ec84f7aa9e1d06"
dependencies = [
"approx",
"matrixmultiply",
"num-complex",
"num-rational",
"num-traits",
"serde",
"simba",
"typenum",
]
[[package]]
name = "num-complex"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
dependencies = [
"num-traits",
"serde",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "regex"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "rust-ffi"
version = "0.1.0"
dependencies = [
"enso-parser",
"serde_json",
"wasm-bindgen",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.188"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.31",
]
[[package]]
name = "serde_json"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "simba"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5132a955559188f3d13c9ba831e77c802ddc8782783f050ed0c52f5988b95f4c"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"unicode-xid",
]
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "uuid"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"serde",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.31",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.31",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "weak-table"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549"
[[package]]
name = "web-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [
"js-sys",
"wasm-bindgen",
]

View File

@ -8,18 +8,7 @@ authors = ["Enso Team <contact@enso.org>"]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
wasm-bindgen = { version = "0.2.84", features = [] }
enso-parser = { path = "../../../lib/rust/parser" } enso-parser = { path = "../../../lib/rust/parser" }
serde_json = "1.0" wasm-bindgen = { workspace = true }
serde_json = { workspace = true }
[workspace] console_error_panic_hook = { workspace = true }
[profile.release]
debug = false
strip = true
lto = true
codegen-units = 1
opt-level = "z"
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os']

View File

@ -19,3 +19,8 @@ pub fn parse_to_json(code: &str) -> String {
let ast = PARSER.with(|parser| parser.run(code)); let ast = PARSER.with(|parser| parser.run(code));
serde_json::to_string(&ast).expect("Failed to serialize AST to JSON") serde_json::to_string(&ast).expect("Failed to serialize AST to JSON")
} }
#[wasm_bindgen(start)]
fn main() {
console_error_panic_hook::set_once();
}

61
app/gui2/shared/event.ts Normal file
View File

@ -0,0 +1,61 @@
import * as array from 'lib0/array'
import * as map from 'lib0/map'
import * as set from 'lib0/set'
type EventMap = { [name: PropertyKey]: any[] }
export type EventHandler<Args extends any[] = any[]> = (...args: Args) => void
export class Emitter<Events extends EventMap = EventMap> {
private _observers: Map<PropertyKey, Set<EventHandler<any[]>>>
constructor() {
this._observers = map.create()
}
on<N extends keyof Events>(name: N, f: EventHandler<Events[N]>) {
map.setIfUndefined(this._observers, name as PropertyKey, set.create).add(f)
}
once<N extends keyof Events>(name: N, f: EventHandler<Events[N]>) {
const _f = (...args: Events[N]) => {
this.off(name, _f)
f(...args)
}
this.on(name, _f)
}
off<N extends keyof Events>(name: N, f: EventHandler<Events[N]>) {
const observers = this._observers.get(name)
if (observers !== undefined) {
observers.delete(f as EventHandler<any[]>)
if (observers.size === 0) {
this._observers.delete(name)
}
}
}
emit<N extends keyof Events>(name: N, args: Events[N]) {
// copy all listeners to an array first to make sure that no event is emitted to listeners that
// are subscribed while the event handler is called.
return array
.from((this._observers.get(name) || map.create()).values())
.forEach((f) => f(...args))
}
destroy() {
this._observers = map.create()
}
}
// Partial compatibility with node 'events' module, for the purposes of @open-rpc/client-js.
// See vite.config.ts for details.
export class EventEmitter extends Emitter<EventMap> {
addListener(name: string, f: EventHandler) {
this.on(name, f)
}
removeListener(name: string, f: EventHandler) {
this.off(name, f)
}
removeAllListeners() {
this.destroy()
}
}

View File

@ -0,0 +1,94 @@
import { Client } from '@open-rpc/client-js'
import * as map from 'lib0/map'
import * as set from 'lib0/set'
import { SHA3 } from 'sha3'
import { Emitter } from './event'
import type {
Checksum,
FileEdit,
Notifications,
Path,
RegisterOptions,
response,
} from './languageServerTypes'
import type { Uuid } from './yjsModel'
export class LanguageServer extends Emitter<Notifications> {
client: Client
handlers: Map<string, Set<(...params: any[]) => void>>
constructor(client: Client) {
super()
this.client = client
this.handlers = new Map()
client.onNotification((notification) => {
this.emit(notification.method as keyof Notifications, [notification.params])
})
}
addEventListener<K extends keyof Notifications>(
type: K,
listener: (params: Notifications[K]) => void,
) {
const listeners = map.setIfUndefined(this.handlers, type as string, set.create)
listeners.add(listener)
}
removeEventListener<K extends keyof Notifications>(
type: K,
listener: (params: Notifications[K]) => void,
) {
const listeners = this.handlers.get(type as string)
if (listeners) {
listeners.delete(listener)
}
}
private request(method: string, params: object): Promise<any> {
return this.client.request({ method, params })
}
acquireCapability(method: string, registerOptions: RegisterOptions): Promise<void> {
return this.request('capability/acquire', { method, registerOptions })
}
acquireReceivesTreeUpdates(path: Path): Promise<void> {
return this.acquireCapability('file/receivesTreeUpdates', { path })
}
initProtocolConnection(clientId: Uuid): Promise<response.InitProtocolConnection> {
return this.request('session/initProtocolConnection', { clientId })
}
openTextFile(path: Path): Promise<response.OpenTextFile> {
return this.request('text/openFile', { path })
}
closeTextFile(path: Path): Promise<void> {
return this.request('text/closeFile', { path })
}
saveTextFile(path: Path, currentVersion: Checksum): Promise<void> {
return this.request('text/save', { path, currentVersion })
}
applyEdit(edit: FileEdit, execute: boolean): Promise<void> {
return this.request('text/applyEdit', { edit, execute })
}
listFiles(path: Path): Promise<response.FileList> {
return this.request('file/list', { path })
}
dispose() {
this.client.close()
}
}
export function computeTextChecksum(text: string): Checksum {
const hash = new SHA3(224)
hash.update(text)
return hash.digest('hex') as Checksum
}

View File

@ -0,0 +1,301 @@
import type { Uuid } from './yjsModel'
/** Version checksum of a text file - Sha3_224 */
declare const brandChecksum: unique symbol
export type Checksum = string & { [brandChecksum]: never }
export type ContextId = Uuid
export type ExpressionId = Uuid
export type ContentRoot =
| { type: 'Project'; id: Uuid }
| { type: 'FileSystemRoot'; id: Uuid; path: string }
| { type: 'Home'; id: Uuid }
| { type: 'Library'; id: Uuid; namespace: string; name: string; version: string }
| { type: 'Custom'; id: Uuid }
/** A path is a representation of a path relative to a specified content root. */
export interface Path {
/** Path's root id. */
rootId: Uuid
/** Path's segments. */
segments: string[]
}
export interface FileEdit {
path: Path
edits: TextEdit[]
oldVersion: Checksum
newVersion: Checksum
}
export interface TextEdit {
range: TextRange
text: string
}
export interface TextRange {
start: Position
end: Position
}
export interface Position {
line: number
character: number
}
export type RegisterOptions = { path: Path } | { contextId: ContextId } | {}
export interface CapabilityRegistration {
method: string
register_options: RegisterOptions
}
export type FileEventKind = 'Added' | 'Removed' | 'Modified'
export interface MethodCall {
/** The method pointer of a call. */
methodPointer: MethodPointer
/** Indexes of arguments that have not been applied to this method. */
notAppliedArguments: number[]
}
export type ExpressionUpdatePayload = Value | DataflowError | Panic | Pending
/**
* Indicates that the expression was computed to a value.
*/
export interface Value {
/**
* Information about attached warnings.
*/
warnings?: Warnings
/**
* The schema of returned function value.
*/
functionSchema?: FunctionSchema
}
/**
* Indicates that the expression was computed to an error.
*/
export interface DataflowError {
/**
* The list of expressions leading to the root error.
*/
trace: ExpressionId[]
}
/**
* Indicates that the expression failed with the runtime exception.
*/
export interface Panic {
/**
* The error message.
*/
message: string
/**
* The stack trace.
*/
trace: ExpressionId[]
}
/**
* Indicates the expression is currently being computed. Optionally it
* provides description and percentage (`0.0-1.0`) of completeness.
*/
export interface Pending {
/**
* Optional message describing current operation.
*/
message?: string
/**
* Optional amount of already done work as a number between `0.0` to `1.0`.
*/
progress?: Number
}
/**
* Information about warnings associated with the value.
*/
export interface Warnings {
/**
* The number of attached warnings.
*/
count: number
/**
* If the value has a single warning attached, this field contains textual
* representation of the attached warning. In general, warning values should
* be obtained by attaching an appropriate visualization to a value.
*/
value?: string
}
/**
* Contains a method pointer with information on the partially applied argument
* positions.
*/
export interface FunctionSchema {
/** The method pointer of this function. */
methodPointer: MethodPointer
/** Indexes of arguments that have not been applied to this function. */
notAppliedArguments: number[]
}
export interface MethodPointer {
/** The fully qualified module name. */
module: String
/** The type on which the method is defined. */
definedOnType: String
/** The method name. */
name: String
}
export type ProfilingInfo = ExecutionTime
interface ExecutionTime {
/** The time elapsed during the expression's evaluation, in nanoseconds */
nanoTime: Number
}
interface ExpressionUpdate {
/** The id of updated expression. */
expressionId: ExpressionId
/** The updated type of the expression. */
type?: String
/** The updated method call info. */
methodCall?: MethodCall
/** Profiling information about the expression. */
profilingInfo: ProfilingInfo[]
/** Wether or not the expression's value came from the cache. */
fromCache: boolean
/** An extra information about the computed value. */
payload: ExpressionUpdatePayload
}
interface StackTraceElement {
functionName: string
path?: Path
location?: TextRange
}
type DiagnosticType = 'Error' | 'Warning'
interface Diagnostic {
/**
* The type of diagnostic message.
*/
kind: DiagnosticType
/**
* The diagnostic message.
*/
message: String
/**
* The location of a file containing the diagnostic.
*/
path?: Path
/**
* The location of the diagnostic object in a file.
*/
location?: Range
/**
* The id of related expression.
*/
expressionId?: ExpressionId
/**
* The stack trace.
*/
stack: StackTraceElement[]
}
/** A representation of what kind of type a filesystem object can be. */
type FileSystemObject =
| {
type: 'Directory'
name: string
path: Path
}
/**
* A directory which contents have been truncated, i.e. with its subtree not listed any further
* due to depth limit being reached.
*/
| {
type: 'DirectoryTruncated'
name: string
path: Path
}
| {
type: 'File'
name: string
path: Path
}
/** Represents other, potentially unrecognized object. Example is a broken symbolic link. */
| {
type: 'Other'
name: string
path: Path
}
/** Represents a symbolic link that creates a loop. */
| {
type: 'SymlinkLoop'
name: string
path: Path
/** A target of the symlink. Since it is a loop, target is a subpath of the symlink. */
target: Path
}
interface VisualizationContext {}
export type Notifications = {
'file/event': [{ path: Path; kind: FileEventKind }]
'text/autoSave': [{ path: Path }]
'text/didChange': [{ edits: FileEdit[] }]
'text/fileModifiedOnDisk': [{ path: Path }]
'executionContext/expressionUpdates': [{ contextId: ContextId; updates: ExpressionUpdate[] }]
'executionContext/executionFailed': [{ contextId: ContextId; message: string }]
'executionContext/executionComplete': [{ contextId: ContextId }]
'executionContext/executionStatus': [{ contextId: ContextId; diagnostics: Diagnostic[] }]
'search/suggestionsDatabaseUpdate': [{}]
'file/rootAdded': [{}]
'file/rootRemoved': [{}]
'executionContext/visualizationEvaluationFailed': [
{
contextId: ContextId
visualizationId: Uuid
expressionId: ExpressionId
message: String
diagnostic?: Diagnostic
},
]
'refactoring/projectRenamed': [{}]
}
export namespace response {
export interface OpenTextFile {
writeCapability: CapabilityRegistration | null
content: string
currentVersion: Checksum
}
export interface InitProtocolConnection {
contentRoots: ContentRoot[]
}
export interface FileList {
paths: FileSystemObject[]
}
export interface VisualizationUpdate {
context: VisualizationContext
data: Uint8Array
}
}

View File

@ -1,361 +0,0 @@
import * as y from 'yjs'
import { setIfUndefined } from 'lib0/map'
import { isSome, type Opt } from '@/util/opt'
export type Uuid = ReturnType<typeof crypto.randomUUID>
declare const brandExprId: unique symbol
export type ExprId = Uuid & { [brandExprId]: never }
export const NULL_EXPR_ID: ExprId = '00000000-0000-0000-0000-000000000000' as ExprId
export interface NodeMetadata {
x: number
y: number
vis?: string
}
const enum NamedDocKey {
NAME = 'name',
DOC = 'doc',
}
interface NamedDoc {
name: y.Text
doc: y.Doc
}
type NamedDocMap = y.Map<y.Text | y.Doc>
export class NamedDocArray {
array: y.Array<NamedDocMap>
nameToIndex: Map<string, y.RelativePosition[]>
constructor(doc: y.Doc, name: string) {
this.array = doc.getArray(name)
this.nameToIndex = new Map()
this.array.forEach(this.integrateAddedItem.bind(this))
}
private integrateAddedItem(item: NamedDocMap, index: number): void {
const name = item.get(NamedDocKey.NAME)?.toString()
if (name == null) return
const indices = setIfUndefined(this.nameToIndex, name, () => [] as y.RelativePosition[])
indices.push(y.createRelativePositionFromTypeIndex(this.array, index))
}
names(): string[] {
return this.array.map((item) => item.get(NamedDocKey.NAME)?.toString()).filter(isSome)
}
createNew(name: string): NamedDoc {
const map = new y.Map<y.Text | y.Doc>()
const yName = map.set(NamedDocKey.NAME, new y.Text(name))
const doc = map.set(NamedDocKey.DOC, new y.Doc())
this.array.push([map])
this.integrateAddedItem(map, this.array.length - 1)
return { name: yName, doc }
}
open(name: string): NamedDoc | undefined {
const index = this.names().indexOf(name)
if (index < 0) return
const item = this.array.get(index)
if (item == null) return
const yName = item.get(NamedDocKey.NAME)
const doc = item.get(NamedDocKey.DOC)
if (!(yName instanceof y.Text && doc instanceof y.Doc)) return
return { name: yName, doc }
}
delete(name: string): void {
const relPos = this.nameToIndex.get(name)
if (relPos == null) return
const pos = y.createAbsolutePositionFromRelativePosition(relPos[0], this.array.doc!)
if (pos == null) return
this.array.delete(pos.index, 1)
this.nameToIndex.delete(name)
}
dispose(): void {}
}
export class DistributedModel {
doc: y.Doc
projects: NamedDocArray
constructor(doc: y.Doc) {
this.doc = doc
this.projects = new NamedDocArray(this.doc, 'projects')
}
projectNames(): string[] {
return this.projects.names()
}
async openProject(name: string): Promise<Opt<DistributedProject>> {
await this.doc.whenLoaded
const pair = this.projects.open(name)
if (pair == null) return null
return await DistributedProject.load(pair)
}
async createNewProject(name: string): Promise<DistributedProject> {
await this.doc.whenLoaded
const pair = this.projects.createNew(name)
return await DistributedProject.load(pair)
}
async openOrCreateProject(name: string): Promise<DistributedProject> {
return (await this.openProject(name)) ?? (await this.createNewProject(name))
}
deleteProject(name: string): void {
this.projects.delete(name)
}
dispose(): void {
this.projects.dispose()
}
}
export class DistributedProject {
doc: y.Doc
name: y.Text
modules: NamedDocArray
static async load(pair: NamedDoc) {
const project = new DistributedProject(pair)
project.doc.load()
await project.doc.whenLoaded
return project
}
private constructor(pair: NamedDoc) {
this.doc = pair.doc
this.name = pair.name
this.modules = new NamedDocArray(this.doc, 'modules')
}
moduleNames(): string[] {
return this.modules.names()
}
async openModule(name: string): Promise<DistributedModule | null> {
const pair = this.modules.open(name)
if (pair == null) return null
return await DistributedModule.load(pair)
}
async createNewModule(name: string): Promise<DistributedModule> {
const pair = this.modules.createNew(name)
return await DistributedModule.load(pair)
}
async openOrCreateModule(name: string): Promise<DistributedModule> {
return (await this.openModule(name)) ?? (await this.createNewModule(name))
}
deleteModule(name: string): void {
this.modules.delete(name)
}
}
class DistributedModule {
name: y.Text
doc: y.Doc
contents: y.Text
idMap: y.Map<[any, any]>
metadata: y.Map<NodeMetadata>
static async load(pair: NamedDoc) {
const module = new DistributedModule(pair)
module.doc.load()
await module.doc.whenLoaded
return module
}
private constructor(pair: NamedDoc) {
this.name = pair.name
this.doc = pair.doc
this.contents = this.doc.getText('contents')
this.idMap = this.doc.getMap('idMap')
this.metadata = this.doc.getMap('metadata')
}
insertNewNode(offset: number, content: string, meta: NodeMetadata): ExprId {
const range = [offset, offset + content.length]
const newId = crypto.randomUUID() as ExprId
this.doc.transact(() => {
this.contents.insert(offset, content + '\n')
const start = y.createRelativePositionFromTypeIndex(this.contents, range[0])
const end = y.createRelativePositionFromTypeIndex(this.contents, range[1])
const startJson = y.relativePositionToJSON(start)
const endJson = y.relativePositionToJSON(end)
this.idMap.set(newId, [startJson, endJson])
this.metadata.set(newId, meta)
})
return newId
}
replaceExpressionContent(id: ExprId, content: string, range?: ContentRange): void {
const exprRangeJson = this.idMap.get(id)
if (exprRangeJson == null) return
const exprRange = [
y.createRelativePositionFromJSON(exprRangeJson[0]),
y.createRelativePositionFromJSON(exprRangeJson[1]),
]
const exprStart = y.createAbsolutePositionFromRelativePosition(exprRange[0], this.doc)?.index
const exprEnd = y.createAbsolutePositionFromRelativePosition(exprRange[1], this.doc)?.index
if (exprStart == null || exprEnd == null) return
const start = range == null ? exprStart : exprStart + range[0]
const end = range == null ? exprEnd : exprStart + range[1]
if (start > end) throw new Error('Invalid range')
if (start < exprStart || end > exprEnd) throw new Error('Range out of bounds')
this.doc.transact(() => {
if (content.length > 0) {
this.contents.insert(start, content)
}
if (start !== end) {
this.contents.delete(start + content.length, end - start)
}
})
}
updateNodeMetadata(id: ExprId, meta: Partial<NodeMetadata>): void {
const existing = this.metadata.get(id) ?? { x: 0, y: 0 }
this.metadata.set(id, { ...existing, ...meta })
}
getIdMap(): IdMap {
return new IdMap(this.idMap, this.contents)
}
dispose(): void {
this.doc.destroy()
}
}
export interface RelativeRange {
start: y.RelativePosition
end: y.RelativePosition
}
/**
* Accessor for the ID map stored in shared yjs map as relative ranges. Synchronizes the ranges
* that were accessed during parsing, throws away stale ones. The text contents is used to translate
* the relative ranges to absolute ranges, but it is not modified.
*/
export class IdMap {
private contents: y.Text
private doc: y.Doc
private yMap: y.Map<[any, any]>
private rangeToExpr: Map<string, ExprId>
private accessed: Set<ExprId>
private finished: boolean
constructor(yMap: y.Map<[any, any]>, contents: y.Text) {
if (yMap.doc == null) {
throw new Error('IdMap must be associated with a document')
}
this.doc = yMap.doc
this.contents = contents
this.yMap = yMap
this.rangeToExpr = new Map()
this.accessed = new Set()
yMap.forEach((range, expr) => {
if (!isUuid(expr)) return
const indices = this.modelToIndices(range)
if (indices == null) return
this.rangeToExpr.set(IdMap.keyForRange(indices), expr as ExprId)
})
this.finished = false
}
private static keyForRange(range: [number, number]): string {
return `${range[0]}:${range[1]}`
}
private modelToIndices(range: [any, any]): [number, number] | null {
const relStart = y.createRelativePositionFromJSON(range[0])
const relEnd = y.createRelativePositionFromJSON(range[1])
const start = y.createAbsolutePositionFromRelativePosition(relStart, this.doc)
const end = y.createAbsolutePositionFromRelativePosition(relEnd, this.doc)
if (start == null || end == null) return null
return [start.index, end.index]
}
getOrInsertUniqueId(range: [number, number]): ExprId {
if (this.finished) {
throw new Error('IdMap already finished')
}
const key = IdMap.keyForRange(range)
const val = this.rangeToExpr.get(key)
if (val !== undefined) {
this.accessed.add(val)
return val
} else {
const newId = crypto.randomUUID() as ExprId
this.rangeToExpr.set(key, newId)
this.accessed.add(newId)
return newId
}
}
accessedSoFar(): ReadonlySet<ExprId> {
return this.accessed
}
/**
* Finish accessing or modifying ID map. Synchronizes the accessed keys back to the shared map,
* removes keys that were present previously, but were not accessed.
*
* Can be called at most once. After calling this method, the ID map is no longer usable.
*/
finishAndSynchronize(): void {
if (this.finished) {
throw new Error('IdMap already finished')
}
this.finished = true
const doc = this.doc
doc.transact(() => {
this.yMap.forEach((currentRange, expr) => {
// Expressions that were accessed and present in the map are guaranteed to match. There is
// no mechanism for modifying them, so we don't need to check for equality. We only need to
// delete the expressions ones that are not used anymore.
if (!this.accessed.delete(expr as ExprId)) {
this.yMap.delete(expr)
}
})
this.rangeToExpr.forEach((expr, key) => {
// For all remaining expressions, we need to write them into the map.
if (!this.accessed.has(expr)) return
const range = key.split(':').map((x) => parseInt(x, 10)) as [number, number]
const start = y.createRelativePositionFromTypeIndex(this.contents, range[0])
const end = y.createRelativePositionFromTypeIndex(this.contents, range[1])
const startJson = y.relativePositionToJSON(start)
const endJson = y.relativePositionToJSON(end)
this.yMap.set(expr, [startJson, endJson])
})
})
}
}
const uuidRegex = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/
export function isUuid(x: any): x is Uuid {
return typeof x === 'string' && x.length === 36 && uuidRegex.test(x)
}
export type ContentRange = [number, number]
export function rangeEncloses(a: ContentRange, b: ContentRange): boolean {
return a[0] <= b[0] && a[1] >= b[1]
}
export function rangeIntersects(a: ContentRange, b: ContentRange): boolean {
return a[0] <= b[1] && a[1] >= b[0]
}

355
app/gui2/shared/yjsModel.ts Normal file
View File

@ -0,0 +1,355 @@
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import * as Y from 'yjs'
export type Uuid = `${string}-${string}-${string}-${string}-${string}`
declare const brandExprId: unique symbol
export type ExprId = Uuid & { [brandExprId]: never }
export const NULL_EXPR_ID: ExprId = '00000000-0000-0000-0000-000000000000' as ExprId
export interface NodeMetadata {
x: number
y: number
vis?: string
}
export class DistributedProject {
doc: Y.Doc
name: Y.Text
modules: Y.Map<Y.Doc>
constructor(doc: Y.Doc) {
this.doc = doc
this.name = this.doc.getText('name')
this.modules = this.doc.getMap('modules')
}
moduleNames(): string[] {
return Array.from(this.modules.keys())
}
findModuleByDocId(id: string): string | null {
for (const [name, doc] of this.modules.entries()) {
if (doc.guid === id) return name
}
return null
}
async openModule(name: string): Promise<DistributedModule | null> {
const doc = this.modules.get(name)
if (doc == null) return null
return await DistributedModule.load(doc)
}
openUnloadedModule(name: string): DistributedModule | null {
const doc = this.modules.get(name)
if (doc == null) return null
return new DistributedModule(doc)
}
createUnloadedModule(name: string, doc: Y.Doc): DistributedModule {
this.modules.set(name, doc)
return new DistributedModule(doc)
}
async createNewModule(name: string): Promise<DistributedModule> {
return this.createUnloadedModule(name, new Y.Doc())
}
async openOrCreateModule(name: string): Promise<DistributedModule> {
return (await this.openModule(name)) ?? (await this.createNewModule(name))
}
deleteModule(name: string): void {
this.modules.delete(name)
}
dispose(): void {
this.doc.destroy()
}
}
export class DistributedModule {
doc: Y.Doc
contents: Y.Text
idMap: Y.Map<Uint8Array>
metadata: Y.Map<NodeMetadata>
static async load(doc: Y.Doc) {
doc.load()
await doc.whenLoaded
return new DistributedModule(doc)
}
constructor(doc: Y.Doc) {
this.doc = doc
this.contents = this.doc.getText('contents')
this.idMap = this.doc.getMap('idMap')
this.metadata = this.doc.getMap('metadata')
}
insertNewNode(offset: number, content: string, meta: NodeMetadata): ExprId {
const range = [offset, offset + content.length]
const newId = crypto.randomUUID() as ExprId
this.doc.transact(() => {
this.contents.insert(offset, content + '\n')
const start = Y.createRelativePositionFromTypeIndex(this.contents, range[0])
const end = Y.createRelativePositionFromTypeIndex(this.contents, range[1])
this.idMap.set(newId, encodeRange([start, end]))
this.metadata.set(newId, meta)
})
return newId
}
replaceExpressionContent(id: ExprId, content: string, range?: ContentRange): void {
const rangeBuffer = this.idMap.get(id)
if (rangeBuffer == null) return
const [relStart, relEnd] = decodeRange(rangeBuffer)
const exprStart = Y.createAbsolutePositionFromRelativePosition(relStart, this.doc)?.index
const exprEnd = Y.createAbsolutePositionFromRelativePosition(relEnd, this.doc)?.index
if (exprStart == null || exprEnd == null) return
const start = range == null ? exprStart : exprStart + range[0]
const end = range == null ? exprEnd : exprStart + range[1]
if (start > end) throw new Error('Invalid range')
if (start < exprStart || end > exprEnd) throw new Error('Range out of bounds')
this.doc.transact(() => {
if (content.length > 0) {
this.contents.insert(start, content)
}
if (start !== end) {
this.contents.delete(start + content.length, end - start)
}
})
}
updateNodeMetadata(id: ExprId, meta: Partial<NodeMetadata>): void {
const existing = this.metadata.get(id) ?? { x: 0, y: 0 }
this.metadata.set(id, { ...existing, ...meta })
}
getIdMap(): IdMap {
return new IdMap(this.idMap, this.contents)
}
dispose(): void {
this.doc.destroy()
}
}
export type RelativeRange = [Y.RelativePosition, Y.RelativePosition]
/**
* Accessor for the ID map stored in shared yjs map as relative ranges. Synchronizes the ranges
* that were accessed during parsing, throws away stale ones. The text contents is used to translate
* the relative ranges to absolute ranges, but it is not modified.
*/
export class IdMap {
private contents: Y.Text
private doc: Y.Doc
private yMap: Y.Map<Uint8Array>
private rangeToExpr: Map<string, ExprId>
private accessed: Set<ExprId>
private finished: boolean
constructor(yMap: Y.Map<Uint8Array>, contents: Y.Text) {
if (yMap.doc == null) {
throw new Error('IdMap must be associated with a document')
}
this.doc = yMap.doc
this.contents = contents
this.yMap = yMap
this.rangeToExpr = new Map()
this.accessed = new Set()
yMap.forEach((rangeBuffer, expr) => {
if (!(isUuid(expr) && rangeBuffer instanceof Uint8Array)) return
const indices = this.modelToIndices(rangeBuffer)
if (indices == null) return
this.rangeToExpr.set(IdMap.keyForRange(indices), expr as ExprId)
})
this.finished = false
}
private static keyForRange(range: [number, number]): string {
return `${range[0].toString(16)}:${range[1].toString(16)}`
}
private static rangeForKey(key: string): [number, number] {
return key.split(':').map((x) => parseInt(x, 16)) as [number, number]
}
private modelToIndices(rangeBuffer: Uint8Array): [number, number] | null {
const [relStart, relEnd] = decodeRange(rangeBuffer)
const start = Y.createAbsolutePositionFromRelativePosition(relStart, this.doc)
const end = Y.createAbsolutePositionFromRelativePosition(relEnd, this.doc)
if (start == null || end == null) return null
return [start.index, end.index]
}
insertKnownId(range: [number, number], id: ExprId) {
if (this.finished) {
throw new Error('IdMap already finished')
}
const key = IdMap.keyForRange(range)
this.rangeToExpr.set(key, id)
this.accessed.add(id)
}
getOrInsertUniqueId(range: [number, number]): ExprId {
if (this.finished) {
throw new Error('IdMap already finished')
}
const key = IdMap.keyForRange(range)
const val = this.rangeToExpr.get(key)
if (val !== undefined) {
this.accessed.add(val)
return val
} else {
const newId = crypto.randomUUID() as ExprId
this.rangeToExpr.set(key, newId)
this.accessed.add(newId)
return newId
}
}
accessedSoFar(): ReadonlySet<ExprId> {
return this.accessed
}
toRawRanges(): Record<string, [number, number]> {
const ranges: Record<string, [number, number]> = {}
for (const [key, expr] of this.rangeToExpr.entries()) {
ranges[expr] = IdMap.rangeForKey(key)
}
return ranges
}
/**
* Finish accessing or modifying ID map. Synchronizes the accessed keys back to the shared map,
* removes keys that were present previously, but were not accessed.
*
* Can be called at most once. After calling this method, the ID map is no longer usable.
*/
finishAndSynchronize(): void {
if (this.finished) {
throw new Error('IdMap already finished')
}
this.finished = true
const doc = this.doc
doc.transact(() => {
this.yMap.forEach((_, expr) => {
// Expressions that were accessed and present in the map are guaranteed to match. There is
// no mechanism for modifying them, so we don't need to check for equality. We only need to
// delete the expressions ones that are not used anymore.
if (!this.accessed.delete(expr as ExprId)) {
this.yMap.delete(expr)
}
})
this.rangeToExpr.forEach((expr, key) => {
// For all remaining expressions, we need to write them into the map.
if (!this.accessed.has(expr)) return
const range = IdMap.rangeForKey(key)
const start = Y.createRelativePositionFromTypeIndex(this.contents, range[0])
const end = Y.createRelativePositionFromTypeIndex(this.contents, range[1])
const encoded = encodeRange([start, end])
this.yMap.set(expr, encoded)
})
})
}
}
function encodeRange(range: RelativeRange): Uint8Array {
const encoder = encoding.createEncoder()
const start = Y.encodeRelativePosition(range[0])
const end = Y.encodeRelativePosition(range[1])
encoding.writeUint8(encoder, start.length)
encoding.writeUint8Array(encoder, start)
encoding.writeUint8(encoder, end.length)
encoding.writeUint8Array(encoder, end)
return encoding.toUint8Array(encoder)
}
function decodeRange(buffer: Uint8Array): RelativeRange {
const decoder = decoding.createDecoder(buffer)
const startLen = decoding.readUint8(decoder)
const start = decoding.readUint8Array(decoder, startLen)
const endLen = decoding.readUint8(decoder)
const end = decoding.readUint8Array(decoder, endLen)
return [Y.decodeRelativePosition(start), Y.decodeRelativePosition(end)]
}
const uuidRegex = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/
export function isUuid(x: any): x is Uuid {
return typeof x === 'string' && x.length === 36 && uuidRegex.test(x)
}
export type ContentRange = [number, number]
export function rangeEncloses(a: ContentRange, b: ContentRange): boolean {
return a[0] <= b[0] && a[1] >= b[1]
}
export function rangeIntersects(a: ContentRange, b: ContentRange): boolean {
return a[0] <= b[1] && a[1] >= b[0]
}
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest
type RangeTest = { a: ContentRange; b: ContentRange }
const equalRanges: RangeTest[] = [
{ a: [0, 0], b: [0, 0] },
{ a: [0, 1], b: [0, 1] },
{ a: [-5, 5], b: [-5, 5] },
]
const totalOverlap: RangeTest[] = [
{ a: [0, 1], b: [0, 0] },
{ a: [0, 2], b: [2, 2] },
{ a: [-1, 1], b: [1, 1] },
{ a: [0, 2], b: [0, 1] },
{ a: [-10, 10], b: [-3, 7] },
{ a: [0, 5], b: [1, 2] },
{ a: [3, 5], b: [3, 4] },
]
const reverseTotalOverlap: RangeTest[] = totalOverlap.map(({ a, b }) => ({ a: b, b: a }))
const noOverlap: RangeTest[] = [
{ a: [0, 1], b: [2, 3] },
{ a: [0, 1], b: [-1, -1] },
{ a: [5, 6], b: [2, 3] },
{ a: [0, 2], b: [-2, -1] },
{ a: [-5, -3], b: [9, 10] },
{ a: [-3, 2], b: [3, 4] },
]
const partialOverlap: RangeTest[] = [
{ a: [0, 3], b: [-1, 1] },
{ a: [0, 1], b: [-1, 0] },
{ a: [0, 0], b: [-1, 0] },
{ a: [0, 2], b: [1, 4] },
{ a: [-8, 0], b: [0, 10] },
]
test.each([...equalRanges, ...totalOverlap])('Range $a should enclose $b', ({ a, b }) =>
expect(rangeEncloses(a, b)).toBe(true),
)
test.each([...noOverlap, ...partialOverlap, ...reverseTotalOverlap])(
'Range $a should not enclose $b',
({ a, b }) => expect(rangeEncloses(a, b)).toBe(false),
)
test.each([...equalRanges, ...totalOverlap, ...reverseTotalOverlap, ...partialOverlap])(
'Range $a should intersect $b',
({ a, b }) => expect(rangeIntersects(a, b)).toBe(true),
)
test.each([...noOverlap])('Range $a should not intersect $b', ({ a, b }) =>
expect(rangeIntersects(a, b)).toBe(false),
)
}

View File

@ -1,7 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue' import { provideGuiConfig, type GuiConfig } from '@/providers/guiConfig'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase' import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import ProjectView from '@/views/ProjectView.vue' import ProjectView from '@/views/ProjectView.vue'
import { onMounted, toRef } from 'vue'
const props = defineProps<{
config: GuiConfig
}>()
provideGuiConfig(toRef(props, 'config'))
onMounted(() => useSuggestionDbStore().initializeDb()) onMounted(() => useSuggestionDbStore().initializeDb())
</script> </script>

View File

@ -71,9 +71,22 @@ body {
color: var(--color-text); color: var(--color-text);
/* TEMPORARY. Will be replaced with actual background when it is integrated with the dashboard. */ /* TEMPORARY. Will be replaced with actual background when it is integrated with the dashboard. */
background: #e4d4be; background: #e4d4be;
transition: color 0.5s, background-color 0.5s; transition:
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, color 0.5s,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; background-color 0.5s;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 11.5px; font-size: 11.5px;
font-weight: 500; font-weight: 500;
line-height: 174.5%; line-height: 174.5%;

View File

@ -9,4 +9,5 @@ body {
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
font-weight: normal; font-weight: normal;
cursor: default;
} }

View File

@ -1,14 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { makeComponentList, type Component } from '@/components/ComponentBrowser/component'
import { Filtering } from '@/components/ComponentBrowser/filtering'
import { default as SvgIcon } from '@/components/SvgIcon.vue'
import { default as ToggleIcon } from '@/components/ToggleIcon.vue'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import { useApproach } from '@/util/animation'
import { useResizeObserver } from '@/util/events' import { useResizeObserver } from '@/util/events'
import { type Component, makeComponentList } from '@/components/ComponentBrowser/component'
import type { useNavigator } from '@/util/navigator' import type { useNavigator } from '@/util/navigator'
import { Vec2 } from '@/util/vec2' import { Vec2 } from '@/util/vec2'
import { computed, nextTick, onMounted, ref, watch } from 'vue' import { computed, nextTick, onMounted, ref, watch } from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
import ToggleIcon from '@/components/ToggleIcon.vue'
import { useApproach } from '@/util/animation'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import { Filtering } from '@/components/ComponentBrowser/filtering'
const ITEM_SIZE = 32 const ITEM_SIZE = 32
const TOP_BAR_HEIGHT = 32 const TOP_BAR_HEIGHT = 32

View File

@ -1,5 +1,10 @@
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import {
compareSuggestions,
labelOfEntry,
type MatchedSuggestion,
} from '@/components/ComponentBrowser/component'
import { import {
makeCon, makeCon,
makeMethod, makeMethod,
@ -7,13 +12,8 @@ import {
makeModuleMethod, makeModuleMethod,
makeStaticMethod, makeStaticMethod,
} from '@/stores/suggestionDatabase/entry' } from '@/stores/suggestionDatabase/entry'
import {
compareSuggestions,
labelOfEntry,
type MatchedSuggestion,
} from '@/components/ComponentBrowser/component'
import { Filtering } from '../filtering'
import shuffleSeed from 'shuffle-seed' import shuffleSeed from 'shuffle-seed'
import { Filtering } from '../filtering'
test.each([ test.each([
[makeModuleMethod('Standard.Base.Data.read'), 'Data.read'], [makeModuleMethod('Standard.Base.Data.read'), 'Data.read'],

View File

@ -10,8 +10,8 @@ import {
makeStaticMethod, makeStaticMethod,
makeType, makeType,
} from '@/stores/suggestionDatabase/entry' } from '@/stores/suggestionDatabase/entry'
import { Filtering } from '../filtering'
import type { QualifiedName } from '@/util/qualifiedName' import type { QualifiedName } from '@/util/qualifiedName'
import { Filtering } from '../filtering'
test.each([ test.each([
{ ...makeModuleMethod('Standard.Base.Data.read'), groupIndex: 0 }, { ...makeModuleMethod('Standard.Base.Data.read'), groupIndex: 0 },

View File

@ -1,13 +1,13 @@
import { SuggestionDb } from '@/stores/suggestionDatabase'
import { import {
SuggestionKind, SuggestionKind,
type SuggestionEntry, type SuggestionEntry,
type SuggestionId, type SuggestionId,
} from '@/stores/suggestionDatabase/entry' } from '@/stores/suggestionDatabase/entry'
import { SuggestionDb } from '@/stores/suggestionDatabase'
import { Filtering, type MatchResult } from './filtering'
import { qnIsTopElement, qnLastSegment } from '@/util/qualifiedName'
import { compareOpt } from '@/util/compare' import { compareOpt } from '@/util/compare'
import { isSome } from '@/util/opt' import { isSome } from '@/util/opt'
import { qnIsTopElement, qnLastSegment } from '@/util/qualifiedName'
import { Filtering, type MatchResult } from './filtering'
export interface Component { export interface Component {
suggestionId: SuggestionId suggestionId: SuggestionId

View File

@ -108,8 +108,8 @@ class FilteringWithPattern {
if (this.initialsMatchRegex.test(entry.name)) { if (this.initialsMatchRegex.test(entry.name)) {
return { score: MatchTypeScore.NameInitialMatch } return { score: MatchTypeScore.NameInitialMatch }
} }
const matchedAliasInitials = entry.aliases.find((alias) => const matchedAliasInitials = entry.aliases.find(
this.initialsMatchRegex?.test(alias), (alias) => this.initialsMatchRegex?.test(alias),
) )
if (matchedAliasInitials) { if (matchedAliasInitials) {
return { matchedAlias: matchedAliasInitials, score: MatchTypeScore.AliasInitialMatch } return { matchedAlias: matchedAliasInitials, score: MatchTypeScore.AliasInitialMatch }

View File

@ -2,7 +2,7 @@
import type { Edge } from '@/stores/graph' import type { Edge } from '@/stores/graph'
import type { Rect } from '@/stores/rect' import type { Rect } from '@/stores/rect'
import { clamp } from '@vueuse/core' import { clamp } from '@vueuse/core'
import type { ExprId } from 'shared/yjs-model' import type { ExprId } from 'shared/yjsModel'
import { computed } from 'vue' import { computed } from 'vue'
const props = defineProps<{ const props = defineProps<{

View File

@ -5,12 +5,13 @@ import GraphNode from '@/components/GraphNode.vue'
import TopBar from '@/components/TopBar.vue' import TopBar from '@/components/TopBar.vue'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
import { useProjectStore } from '@/stores/project'
import type { Rect } from '@/stores/rect' import type { Rect } from '@/stores/rect'
import { useWindowEvent } from '@/util/events' import { modKey, useWindowEvent } from '@/util/events'
import { useNavigator } from '@/util/navigator' import { useNavigator } from '@/util/navigator'
import { Vec2 } from '@/util/vec2' import { Vec2 } from '@/util/vec2'
import type { ContentRange, ExprId } from 'shared/yjs-model' import type { ContentRange, ExprId } from 'shared/yjsModel'
import { reactive, ref, watchEffect } from 'vue' import { reactive, ref } from 'vue'
const EXECUTION_MODES = ['design', 'live'] const EXECUTION_MODES = ['design', 'live']
@ -19,13 +20,10 @@ const mode = ref('design')
const viewportNode = ref<HTMLElement>() const viewportNode = ref<HTMLElement>()
const navigator = useNavigator(viewportNode) const navigator = useNavigator(viewportNode)
const graphStore = useGraphStore() const graphStore = useGraphStore()
const projectStore = useProjectStore()
const componentBrowserVisible = ref(false) const componentBrowserVisible = ref(false)
const componentBrowserPosition = ref(Vec2.Zero()) const componentBrowserPosition = ref(Vec2.Zero())
watchEffect(() => {
console.log(`execution mode changed to '${mode.value}'.`)
})
const nodeRects = reactive(new Map<ExprId, Rect>()) const nodeRects = reactive(new Map<ExprId, Rect>())
const exprRects = reactive(new Map<ExprId, Rect>()) const exprRects = reactive(new Map<ExprId, Rect>())
@ -53,20 +51,28 @@ function keyboardBusy() {
useWindowEvent('keydown', (e) => { useWindowEvent('keydown', (e) => {
if (keyboardBusy()) return if (keyboardBusy()) return
const pos = navigator.sceneMousePos const pos = navigator.sceneMousePos
if (pos == null) return
switch (e.key) { if (modKey(e)) {
case 'Enter': switch (e.key) {
if (!componentBrowserVisible.value) { case 'z':
componentBrowserPosition.value = pos projectStore.undoManager.undo()
componentBrowserVisible.value = true break
case 'y':
projectStore.undoManager.redo()
break
}
} else {
switch (e.key) {
case 'Enter':
if (pos != null && !componentBrowserVisible.value) {
componentBrowserPosition.value = pos
componentBrowserVisible.value = true
}
break
case 'n': {
if (pos != null) graphStore.createNode(pos, 'hello "world"! 123 + x')
break
} }
break
case 'n': {
const n = graphStore.createNode(pos)
if (n == null) return
graphStore.setNodeContent(n, 'hello "world"! 123 + x')
break
} }
} }
}) })
@ -130,7 +136,7 @@ function moveNode(id: ExprId, delta: Vec2) {
.viewport { .viewport {
position: relative; position: relative;
contain: layout; contain: layout;
overflow: hidden; overflow: clip;
} }
svg { svg {

View File

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import NodeSpan from '@/components/NodeSpan.vue' import NodeSpan from '@/components/NodeSpan.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import type { Node } from '@/stores/graph' import type { Node } from '@/stores/graph'
import { Rect } from '@/stores/rect' import { Rect } from '@/stores/rect'
import { usePointer, useResizeObserver } from '@/util/events' import { usePointer, useResizeObserver } from '@/util/events'
import { computed, onUpdated, reactive, ref, watch, watchEffect } from 'vue'
import type { ContentRange, ExprId } from 'shared/yjs-model'
import type { Vec2 } from '@/util/vec2' import type { Vec2 } from '@/util/vec2'
import type { ContentRange, ExprId } from 'shared/yjsModel'
import { computed, onUpdated, reactive, ref, watch, watchEffect } from 'vue'
const props = defineProps<{ const props = defineProps<{
node: Node node: Node
@ -261,7 +261,7 @@ function handleClick(e: PointerEvent) {
:class="{ dragging: dragPointer.dragging }" :class="{ dragging: dragPointer.dragging }"
v-on="dragPointer.events" v-on="dragPointer.events"
> >
<div class="icon" @pointerdown="handleClick">@ &nbsp;</div> <SvgIcon class="icon" name="number_input" @pointerdown="handleClick"></SvgIcon>
<div class="binding" @pointerdown.stop>{{ node.binding }}</div> <div class="binding" @pointerdown.stop>{{ node.binding }}</div>
<div <div
ref="editableRootNode" ref="editableRootNode"
@ -283,6 +283,7 @@ function handleClick(e: PointerEvent) {
<style scoped> <style scoped>
.Node { .Node {
color: red;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -300,6 +301,7 @@ function handleClick(e: PointerEvent) {
.binding { .binding {
margin-right: 10px; margin-right: 10px;
color: black;
position: absolute; position: absolute;
right: 100%; right: 100%;
top: 50%; top: 50%;
@ -314,6 +316,7 @@ function handleClick(e: PointerEvent) {
.icon { .icon {
cursor: grab; cursor: grab;
margin-right: 10px;
} }
.Node.dragging, .Node.dragging,

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -4,7 +4,7 @@ const emit = defineEmits<{ click: [] }>()
</script> </script>
<template> <template>
<div class="NavBreadcrumb"><span @click="emit('click')" v-text="text"></span></div> <div class="NavBreadcrumb"><span @click="emit('click')" v-text="props.text"></span></div>
</template> </template>
<style scoped> <style scoped>

View File

@ -3,7 +3,7 @@ import { spanKindName, type Span } from '@/stores/graph'
import { Rect } from '@/stores/rect' import { Rect } from '@/stores/rect'
import { useResizeObserver } from '@/util/events' import { useResizeObserver } from '@/util/events'
import { Vec2 } from '@/util/vec2' import { Vec2 } from '@/util/vec2'
import type { ExprId } from 'shared/yjs-model' import type { ExprId } from 'shared/yjsModel'
import { computed, onUpdated, ref, shallowRef, watch } from 'vue' import { computed, onUpdated, ref, shallowRef, watch } from 'vue'
const props = defineProps<{ const props = defineProps<{

View File

@ -7,10 +7,10 @@ const emit = defineEmits<{ execute: []; 'update:mode': [mode: string] }>()
<template> <template>
<div class="ProjectTitle"> <div class="ProjectTitle">
<span class="title" v-text="title"></span> <span class="title" v-text="props.title"></span>
<ExecutionModeSelector <ExecutionModeSelector
:modes="modes" :modes="props.modes"
:model-value="mode" :model-value="props.mode"
@execute="emit('execute')" @execute="emit('execute')"
@update:modelValue="emit('update:mode', $event)" @update:modelValue="emit('update:mode', $event)"
/> />

View File

@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import NavBar from '@/components/NavBar.vue' import NavBar from '@/components/NavBar.vue'
import ProjectTitle from '@/components/ProjectTitle.vue' import ProjectTitle from '@/components/ProjectTitle.vue'
import { useGuiConfig } from '@/providers/guiConfig'
import { computed } from 'vue'
const props = defineProps<{ title: string; breadcrumbs: string[]; modes: string[]; mode: string }>() const props = defineProps<{ title: string; breadcrumbs: string[]; modes: string[]; mode: string }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -10,19 +12,28 @@ const emit = defineEmits<{
breadcrumbClick: [index: number] breadcrumbClick: [index: number]
'update:mode': [mode: string] 'update:mode': [mode: string]
}>() }>()
const config = useGuiConfig()
const barStyle = computed(() => {
const offset = config.value.window?.topBarOffset ?? '0'
return {
marginLeft: `${offset}px`,
}
})
</script> </script>
<template> <template>
<div class="TopBar"> <div class="TopBar" :style="barStyle">
<ProjectTitle <ProjectTitle
:title="title" :title="props.title"
:modes="modes" :modes="props.modes"
:mode="mode" :mode="props.mode"
@update:mode="emit('update:mode', $event)" @update:mode="emit('update:mode', $event)"
@execute="emit('execute')" @execute="emit('execute')"
/> />
<NavBar <NavBar
:breadcrumbs="breadcrumbs" :breadcrumbs="props.breadcrumbs"
@back="emit('back')" @back="emit('back')"
@forward="emit('forward')" @forward="emit('forward')"
@breadcrumbClick="emit('breadcrumbClick', $event)" @breadcrumbClick="emit('breadcrumbClick', $event)"
@ -40,3 +51,4 @@ const emit = defineEmits<{
left: 9px; left: 9px;
} }
</style> </style>
@/providers/guiConfig

View File

@ -4,8 +4,8 @@ const emit = defineEmits<{ 'update:modelValue': [modelValue: boolean] }>()
</script> </script>
<template> <template>
<div class="Checkbox" @click="emit('update:modelValue', !modelValue)"> <div class="Checkbox" @click="emit('update:modelValue', !props.modelValue)">
<div :class="{ hidden: !modelValue }"></div> <div :class="{ hidden: !props.modelValue }"></div>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'
import { PointerButtonMask, usePointer } from '@/util/events' import { PointerButtonMask, usePointer } from '@/util/events'
import { computed, ref } from 'vue'
const props = defineProps<{ modelValue: number; min: number; max: number }>() const props = defineProps<{ modelValue: number; min: number; max: number }>()
const emit = defineEmits<{ 'update:modelValue': [modelValue: number] }>() const emit = defineEmits<{ 'update:modelValue': [modelValue: number] }>()

View File

@ -1,11 +1,90 @@
import 'enso-dashboard/src/tailwind.css'
const INITIAL_URL_KEY = `Enso-initial-url`
import './assets/main.css' import './assets/main.css'
import { createApp } from 'vue' import { isMac } from 'lib0/environment'
import { decodeQueryParams } from 'lib0/url'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { createApp, type App } from 'vue'
import AppRoot from './App.vue'
import App from './App.vue' const params = decodeQueryParams(location.href)
const app = createApp(App) // Temporary hardcode
app.use(createPinia()) const config = {
supportsLocalBackend: true,
supportsDeepLinks: isMac,
shouldUseAuthentication: false,
projectManagerUrl: PROJECT_MANAGER_URL,
initialProjectName: params.project ?? null,
}
app.mount('#app') let app: App | null = null
interface StringConfig {
[key: string]: StringConfig | string
}
async function runApp(config: StringConfig | null, accessToken: string | null, metadata?: object) {
if (app != null) stopApp()
const rootProps = { config, accessToken, metadata }
app = createApp(AppRoot, rootProps)
app.use(createPinia())
app.mount('#app')
}
function stopApp() {
if (app == null) return
app.unmount()
app = null
}
const appRunner = { runApp, stopApp }
const dashboard = await import('enso-authentication')
/** The entrypoint into the IDE. */
function main() {
/** Note: Signing out always redirects to `/`. It is impossible to make this work,
* as it is not possible to distinguish between having just logged out, and explicitly
* opening a page with no URL parameters set.
*
* Client-side routing endpoints are explicitly not supported for live-reload, as they are
* transitional pages that should not need live-reload when running `gui watch`. */
const url = new URL(location.href)
const isInAuthenticationFlow = url.searchParams.has('code') && url.searchParams.has('state')
const authenticationUrl = location.href
if (isInAuthenticationFlow) {
history.replaceState(null, '', localStorage.getItem(INITIAL_URL_KEY))
}
// const configOptions = OPTIONS.clone()
if (isInAuthenticationFlow) {
history.replaceState(null, '', authenticationUrl)
} else {
localStorage.setItem(INITIAL_URL_KEY, location.href)
}
dashboard.run({
appRunner,
logger: console,
supportsLocalBackend: true, // TODO
supportsDeepLinks: false, // TODO
projectManagerUrl: config.projectManagerUrl,
isAuthenticationDisabled: !config.shouldUseAuthentication,
shouldShowDashboard: true,
initialProjectName: config.initialProjectName,
onAuthenticated() {
if (isInAuthenticationFlow) {
const initialUrl = localStorage.getItem(INITIAL_URL_KEY)
if (initialUrl != null) {
// This is not used past this point, however it is set to the initial URL
// to make refreshing work as expected.
history.replaceState(null, '', initialUrl)
}
}
},
})
}
main()

View File

@ -0,0 +1,26 @@
import { inject, provide, type InjectionKey, type Ref } from 'vue'
export interface GuiConfig {
engine?: {
projectManagerUrl?: string
preferredVersion?: string
rpcUrl?: string
dataUrl?: string
}
startup?: {
project?: string
}
window?: { topBarOffset?: string }
}
const provideKey = Symbol('appConfig') as InjectionKey<Ref<GuiConfig>>
export function useGuiConfig(): Ref<GuiConfig> {
const injected = inject(provideKey)
if (injected == null) throw new Error('AppConfig not provided')
return injected
}
export function provideGuiConfig(appConfig: Ref<GuiConfig>) {
provide(provideKey, appConfig)
}

View File

@ -1,5 +1,5 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useCounterStore = defineStore('counter', () => { export const useCounterStore = defineStore('counter', () => {
const count = ref(0) const count = ref(0)

View File

@ -1,25 +1,25 @@
import { computed, reactive, ref, watch, watchEffect } from 'vue' import { assert, assertNever } from '@/util/assert'
import { defineStore } from 'pinia'
import { map, set } from 'lib0'
import { Vec2 } from '@/util/vec2'
import { assertNever, assert } from '@/util/assert'
import { useProjectStore } from './project'
import * as Y from 'yjs'
import { useObserveYjs } from '@/util/crdt' import { useObserveYjs } from '@/util/crdt'
import { parseEnso } from '@/util/ffi'
import type { Opt } from '@/util/opt'
import { Vec2 } from '@/util/vec2'
import * as map from 'lib0/map'
import * as set from 'lib0/set'
import { defineStore } from 'pinia'
import { import {
rangeIntersects,
type ContentRange, type ContentRange,
type ExprId, type ExprId,
type IdMap, type IdMap,
type NodeMetadata, type NodeMetadata,
rangeIntersects, } from 'shared/yjsModel'
} from 'shared/yjs-model' import { computed, reactive, ref, watch, watchEffect } from 'vue'
import type { Opt } from '@/util/opt' import * as Y from 'yjs'
import { parseEnso } from '@/util/ffi' import { useProjectStore } from './project'
export const useGraphStore = defineStore('graph', () => { export const useGraphStore = defineStore('graph', () => {
const proj = useProjectStore() const proj = useProjectStore()
proj.setProjectName('test')
proj.setObservedFileName('Main.enso') proj.setObservedFileName('Main.enso')
const text = computed(() => proj.module?.contents) const text = computed(() => proj.module?.contents)
@ -79,47 +79,50 @@ export const useGraphStore = defineStore('graph', () => {
watchEffect(() => {}) watchEffect(() => {})
function updateState(affectedRanges?: ContentRange[]) { function updateState(affectedRanges?: ContentRange[]) {
if (proj.module == null) return const module = proj.module
const idMap = proj.module.getIdMap() if (module == null) return
const meta = proj.module.metadata module.doc.transact(() => {
const text = proj.module.contents const idMap = module.getIdMap()
const textContentLocal = textContent.value const meta = module.metadata
const parsed = parseBlock(0, textContentLocal, idMap) const text = module.contents
const textContentLocal = textContent.value
const parsed = parseBlock(0, textContentLocal, idMap)
_parsed.value = parsed _parsed.value = parsed
_parsedEnso.value = parseEnso(textContentLocal) _parsedEnso.value = parseEnso(textContentLocal)
const accessed = idMap.accessedSoFar() const accessed = idMap.accessedSoFar()
for (const nodeId of nodes.keys()) { for (const nodeId of nodes.keys()) {
if (!accessed.has(nodeId)) { if (!accessed.has(nodeId)) {
nodeDeleted(nodeId) nodeDeleted(nodeId)
}
}
idMap.finishAndSynchronize()
for (const stmt of parsed) {
const id = stmt.id
const exprRange: ContentRange = [stmt.exprOffset, stmt.exprOffset + stmt.expression.length]
if (affectedRanges != null) {
while (affectedRanges[0]?.[1] < exprRange[0]) {
affectedRanges.shift()
} }
if (affectedRanges.length === 0) break
const nodeAffected = rangeIntersects(exprRange, affectedRanges[0])
if (!nodeAffected) continue
} }
idMap.finishAndSynchronize()
const nodeMeta = meta.get(id) for (const stmt of parsed) {
const nodeContent = textContentLocal.substring(exprRange[0], exprRange[1]) const exprRange: ContentRange = [stmt.exprOffset, stmt.exprOffset + stmt.expression.length]
const node = nodes.get(id)
if (node == null) { if (affectedRanges != null) {
nodeInserted(stmt, text, nodeContent, nodeMeta) while (affectedRanges[0]?.[1] < exprRange[0]) {
} else { affectedRanges.shift()
nodeUpdated(node, stmt, text, nodeContent, nodeMeta) }
if (affectedRanges.length === 0) break
const nodeAffected = rangeIntersects(exprRange, affectedRanges[0])
if (!nodeAffected) continue
}
const nodeId = stmt.expression.id
const node = nodes.get(nodeId)
const nodeMeta = meta.get(nodeId)
const nodeContent = textContentLocal.substring(exprRange[0], exprRange[1])
if (node == null) {
nodeInserted(stmt, text, nodeContent, nodeMeta)
} else {
nodeUpdated(node, stmt, text, nodeContent, nodeMeta)
}
} }
} })
} }
useObserveYjs(metadata, (event) => { useObserveYjs(metadata, (event) => {
@ -129,11 +132,13 @@ export const useGraphStore = defineStore('graph', () => {
const data = meta.get(id) const data = meta.get(id)
const node = nodes.get(id as ExprId) const node = nodes.get(id as ExprId)
if (data != null && node != null) { if (data != null && node != null) {
const pos = new Vec2(data.x, data.y) const pos = new Vec2(data.x, -data.y)
if (!node.position.equals(pos)) { if (!node.position.equals(pos)) {
node.position = pos node.position = pos
} }
} }
} else {
console.log(op)
} }
} }
}) })
@ -142,20 +147,20 @@ export const useGraphStore = defineStore('graph', () => {
const identUsages = reactive(new Map<string, Set<ExprId>>()) const identUsages = reactive(new Map<string, Set<ExprId>>())
function nodeInserted(stmt: Statement, text: Y.Text, content: string, meta: Opt<NodeMetadata>) { function nodeInserted(stmt: Statement, text: Y.Text, content: string, meta: Opt<NodeMetadata>) {
console.log('nodeInserted', stmt.id) const nodeId = stmt.expression.id
const node: Node = { const node: Node = {
content, content,
binding: stmt.binding ?? '', binding: stmt.binding ?? '',
rootSpan: stmt.expression, rootSpan: stmt.expression,
position: meta == null ? Vec2.Zero() : new Vec2(meta.x, meta.y), position: meta == null ? Vec2.Zero() : new Vec2(meta.x, -meta.y),
docRange: [ docRange: [
Y.createRelativePositionFromTypeIndex(text, stmt.exprOffset), Y.createRelativePositionFromTypeIndex(text, stmt.exprOffset),
Y.createRelativePositionFromTypeIndex(text, stmt.exprOffset + stmt.expression.length), Y.createRelativePositionFromTypeIndex(text, stmt.exprOffset + stmt.expression.length),
], ],
} }
identDefinitions.set(node.binding, stmt.id) identDefinitions.set(node.binding, nodeId)
addSpanUsages(stmt.id, node) addSpanUsages(nodeId, node)
nodes.set(stmt.id, node) nodes.set(nodeId, node)
} }
function nodeUpdated( function nodeUpdated(
@ -165,7 +170,6 @@ export const useGraphStore = defineStore('graph', () => {
content: string, content: string,
meta: Opt<NodeMetadata>, meta: Opt<NodeMetadata>,
) { ) {
console.log('nodeUpdated', stmt.id)
clearSpanUsages(stmt.id, node) clearSpanUsages(stmt.id, node)
node.content = content node.content = content
if (node.binding !== stmt.binding) { if (node.binding !== stmt.binding) {
@ -178,8 +182,8 @@ export const useGraphStore = defineStore('graph', () => {
} else { } else {
node.rootSpan = stmt.expression node.rootSpan = stmt.expression
} }
if (meta != null && !node.position.equals(new Vec2(meta.x, meta.y))) { if (meta != null && !node.position.equals(new Vec2(meta.x, -meta.y))) {
node.position = new Vec2(meta.x, meta.y) node.position = new Vec2(meta.x, -meta.y)
} }
} }
@ -249,17 +253,17 @@ export const useGraphStore = defineStore('graph', () => {
return edges return edges
}) })
function createNode(position: Vec2): Opt<ExprId> { function createNode(position: Vec2, expression: string): Opt<ExprId> {
const mod = proj.module const mod = proj.module
if (mod == null) return if (mod == null) return
const { contents } = mod const { contents } = mod
const meta: NodeMetadata = { const meta: NodeMetadata = {
x: position.x, x: position.x,
y: position.y, y: -position.y,
} }
const ident = generateUniqueIdent() const ident = generateUniqueIdent()
const content = `${ident} = x` const content = `${ident} = ${expression}`
return mod.insertNewNode(contents.length, content, meta) return mod.insertNewNode(contents.length, content, meta)
} }
@ -288,7 +292,7 @@ export const useGraphStore = defineStore('graph', () => {
function setNodePosition(id: ExprId, position: Vec2) { function setNodePosition(id: ExprId, position: Vec2) {
const node = nodes.get(id) const node = nodes.get(id)
if (node == null) return if (node == null) return
proj.module?.updateNodeMetadata(id, { x: position.x, y: position.y }) proj.module?.updateNodeMetadata(id, { x: position.x, y: -position.y })
} }
return { return {
@ -398,10 +402,10 @@ interface Statement {
function parseBlock(offset: number, content: string, idMap: IdMap): Statement[] { function parseBlock(offset: number, content: string, idMap: IdMap): Statement[] {
const stmtRegex = /^( *)(([a-zA-Z0-9_]+) *= *)?(.*)$/gm const stmtRegex = /^( *)(([a-zA-Z0-9_]+) *= *)?(.*)$/gm
const stmts: Statement[] = [] const stmts: Statement[] = []
content.replace(stmtRegex, (stmt, ident, beforeExpr, binding, expr, index) => { content.replace(stmtRegex, (stmt, indent, beforeExpr, binding, expr, index) => {
if (stmt.trim().length === 0) return stmt if (stmt.trim().length === 0) return stmt
const pos = offset + index + ident.length const pos = offset + index + indent.length
const id = idMap.getOrInsertUniqueId([pos, pos + stmt.length]) const id = idMap.getOrInsertUniqueId([pos, pos + stmt.length - indent.length])
const exprOffset = pos + (beforeExpr?.length ?? 0) const exprOffset = pos + (beforeExpr?.length ?? 0)
stmts.push({ stmts.push({
id, id,

View File

@ -1,10 +1,32 @@
import { ref, watchEffect } from 'vue' import { useGuiConfig, type GuiConfig } from '@/providers/guiConfig'
import { defineStore } from 'pinia'
import * as Y from 'yjs'
import { attachProvider } from '@/util/crdt' import { attachProvider } from '@/util/crdt'
import { DistributedModel } from 'shared/yjs-model' import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
import { computedAsync } from '@vueuse/core' import { computedAsync } from '@vueuse/core'
import { defineStore } from 'pinia'
import { LanguageServer } from 'shared/languageServer'
import { DistributedProject } from 'shared/yjsModel'
import { markRaw, ref, watchEffect } from 'vue'
import { Awareness } from 'y-protocols/awareness' import { Awareness } from 'y-protocols/awareness'
import * as Y from 'yjs'
interface LsUrls {
rpcUrl: string
dataUrl: string
}
function resolveLsUrl(config: GuiConfig): LsUrls {
const engine = config.engine
if (engine == null) throw new Error('Missing engine configuration')
if (engine.rpcUrl != null && engine.dataUrl != null) {
return {
rpcUrl: engine.rpcUrl,
dataUrl: engine.dataUrl,
}
}
throw new Error('Incomplete engine configuration')
}
/** /**
* The project store synchronizes and holds the open project-related data. The synchronization is * The project store synchronizes and holds the open project-related data. The synchronization is
@ -12,44 +34,77 @@ import { Awareness } from 'y-protocols/awareness'
* client, it is submitted to the language server as a document update. * client, it is submitted to the language server as a document update.
*/ */
export const useProjectStore = defineStore('project', () => { export const useProjectStore = defineStore('project', () => {
// inputs
const projectName = ref<string>()
const observedFileName = ref<string>() const observedFileName = ref<string>()
const doc = new Y.Doc() const doc = new Y.Doc()
const awareness = new Awareness(doc) const awareness = new Awareness(doc)
const config = useGuiConfig()
const projectId = config.value.startup?.project
if (projectId == null) throw new Error('Missing project ID')
const lsUrls = resolveLsUrl(config.value)
const rpcTransport = new WebSocketTransport(lsUrls.rpcUrl)
const rpcRequestManager = new RequestManager([rpcTransport])
const rpcClient = new Client(rpcRequestManager)
const lsRpcConnection = new LanguageServer(rpcClient)
const undoManager = new Y.UndoManager([], { doc })
watchEffect((onCleanup) => { watchEffect((onCleanup) => {
// For now, let's assume that the websocket server is running on the same host as the web server. // For now, let's assume that the websocket server is running on the same host as the web server.
// Eventually, we can make this configurable, or even runtime variable. // Eventually, we can make this configurable, or even runtime variable.
const socketUrl = location.origin.replace(/^http/, 'ws') + '/room' const socketUrl = new URL(location.origin)
const provider = attachProvider(socketUrl, 'enso-projects', doc, awareness) socketUrl.protocol = location.protocol.replace(/^http/, 'ws')
socketUrl.pathname = '/project'
const provider = attachProvider(socketUrl.href, 'index', { ls: lsUrls.rpcUrl }, doc, awareness)
onCleanup(() => { onCleanup(() => {
provider.dispose() provider.dispose()
}) })
}) })
const model = new DistributedModel(doc) const projectModel = new DistributedProject(doc)
const project = computedAsync(async () => { const moduleDocGuid = ref<string>()
const name = projectName.value
function currentDocGuid() {
const name = observedFileName.value
if (name == null) return if (name == null) return
return await model.openOrCreateProject(name) return projectModel.modules.get(name)?.guid
}) }
function tryReadDocGuid() {
const guid = currentDocGuid()
if (guid === moduleDocGuid.value) return
moduleDocGuid.value = guid
}
projectModel.modules.observe((_) => tryReadDocGuid())
watchEffect(tryReadDocGuid)
const module = computedAsync(async () => { const module = computedAsync(async () => {
const moduleName = observedFileName.value const guid = moduleDocGuid.value
const p = project.value if (guid == null) return null
if (moduleName == null || p == null) return const moduleName = projectModel.findModuleByDocId(guid)
return await p.openOrCreateModule(moduleName) if (moduleName == null) return null
return await projectModel.openModule(moduleName)
})
watchEffect((onCleanup) => {
const mod = module.value
if (mod == null) return
const scope: typeof undoManager.scope = [mod.contents, mod.idMap]
undoManager.scope.push(...scope)
onCleanup(() => {
undoManager.scope = undoManager.scope.filter((s) => !scope.includes(s))
})
}) })
return { return {
setProjectName(name: string) {
projectName.value = name
},
setObservedFileName(name: string) { setObservedFileName(name: string) {
observedFileName.value = name observedFileName.value = name
}, },
module, module,
undoManager,
lsRpcConnection: markRaw(lsRpcConnection),
} }
}) })

View File

@ -1,12 +1,12 @@
import { assert } from '@/util/assert' import { assert } from '@/util/assert'
import { import {
isIdentifier,
isQualifiedName,
qnLastSegment, qnLastSegment,
qnParent, qnParent,
qnSplit,
type Identifier, type Identifier,
type QualifiedName, type QualifiedName,
qnSplit,
isQualifiedName,
isIdentifier,
} from '@/util/qualifiedName' } from '@/util/qualifiedName'
export type SuggestionId = number export type SuggestionId = number

View File

@ -1,8 +1,8 @@
import { findIndexOpt } from '@/util/array'
import { isSome } from '@/util/opt'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import { SuggestionKind, type SuggestionEntry, type SuggestionId } from './entry' import { SuggestionKind, type SuggestionEntry, type SuggestionId } from './entry'
import { isSome } from '@/util/opt'
import { findIndexOpt } from '@/util/array'
export type SuggestionDb = Map<SuggestionId, SuggestionEntry> export type SuggestionDb = Map<SuggestionId, SuggestionEntry>
export const SuggestionDb = Map<SuggestionId, SuggestionEntry> export const SuggestionDb = Map<SuggestionId, SuggestionEntry>

View File

@ -1,13 +1,13 @@
export function assertNever(x: never): never { export function assertNever(x: never): never {
throw new Error('Unexpected object: ' + x) bail('Unexpected object: ' + x)
} }
export function assert(condition: boolean): asserts condition { export function assert(condition: boolean): asserts condition {
if (!condition) throw new Error('Assertion failed') if (!condition) bail('Assertion failed')
} }
export function assertUnreachable(): never { export function assertUnreachable(): never {
throw new Error('Unreachable code') bail('Unreachable code')
} }
/** /**

View File

@ -40,8 +40,22 @@ interface SubdocsEvent {
removed: Set<Y.Doc> removed: Set<Y.Doc>
} }
export function attachProvider(url: string, room: string, doc: Y.Doc, awareness: Awareness) { /**
const provider = new WebsocketProvider(url, room, doc, { awareness }) * URL query parameters used in gateway server websocket connection.
*/
export type ProviderParams = {
/** URL for the project's language server RPC connection. */
ls: string
}
export function attachProvider(
url: string,
room: string,
params: ProviderParams,
doc: Y.Doc,
awareness: Awareness,
) {
const provider = new WebsocketProvider(url, room, doc, { awareness, params })
const onSync = () => doc.emit('sync', [true]) const onSync = () => doc.emit('sync', [true])
const onDrop = () => doc.emit('sync', [false]) const onDrop = () => doc.emit('sync', [false])
@ -49,8 +63,7 @@ export function attachProvider(url: string, room: string, doc: Y.Doc, awareness:
function onSubdocs(e: SubdocsEvent) { function onSubdocs(e: SubdocsEvent) {
e.loaded.forEach((subdoc) => { e.loaded.forEach((subdoc) => {
const subdocRoom = `${room}--${subdoc.guid}` attachedSubdocs.set(subdoc, attachProvider(url, subdoc.guid, params, subdoc, awareness))
attachedSubdocs.set(subdoc, attachProvider(url, subdocRoom, subdoc, awareness))
}) })
e.removed.forEach((subdoc) => { e.removed.forEach((subdoc) => {
const subdocProvider = attachedSubdocs.get(subdoc) const subdocProvider = attachedSubdocs.get(subdoc)

View File

@ -4,10 +4,10 @@ import {
onUnmounted, onUnmounted,
proxyRefs, proxyRefs,
ref, ref,
type Ref,
shallowRef, shallowRef,
watch, watch,
watchEffect, watchEffect,
type Ref,
type WatchSource, type WatchSource,
} from 'vue' } from 'vue'
import { Vec2 } from './vec2' import { Vec2 } from './vec2'
@ -108,9 +108,13 @@ export function useDocumentEventConditional<K extends keyof DocumentEventMap>(
}) })
} }
// const hasWindow = typeof window !== 'undefined' const hasWindow = typeof window !== 'undefined'
// const platform = hasWindow ? window.navigator?.platform ?? '' : '' const platform = hasWindow ? window.navigator?.platform ?? '' : ''
// const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(platform) const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(platform)
export function modKey(e: KeyboardEvent): boolean {
return isMacLike ? e.metaKey : e.ctrlKey
}
/** /**
* Get DOM node size and keep it up to date. * Get DOM node size and keep it up to date.

View File

@ -1,7 +1,7 @@
import { Rect } from '@/stores/rect'
import { computed, proxyRefs, ref, type Ref } from 'vue' import { computed, proxyRefs, ref, type Ref } from 'vue'
import { PointerButtonMask, usePointer, useResizeObserver, useWindowEvent } from './events' import { PointerButtonMask, usePointer, useResizeObserver, useWindowEvent } from './events'
import { Vec2 } from './vec2' import { Vec2 } from './vec2'
import { Rect } from '@/stores/rect'
function elemRect(target: Element | undefined): Rect { function elemRect(target: Element | undefined): Rect {
if (target != null && target instanceof Element) { if (target != null && target instanceof Element) {

View File

@ -3,11 +3,18 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.json", "src/**/*.vue", "shared/**/*"], "include": ["env.d.ts", "src/**/*", "src/**/*.json", "src/**/*.vue", "shared/**/*"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"resolvePackageJsonExports": false,
"composite": true, "composite": true,
"outDir": "../../node_modules/.cache/tsc",
"baseUrl": ".", "baseUrl": ".",
"types": ["vitest/importMeta"],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, }
"types": ["vitest/importMeta"] },
} "references": [
{
"path": "../ide-desktop/lib/dashboard/tsconfig.json"
}
]
} }

View File

@ -1,6 +1,9 @@
{ {
"files": [], "files": [],
"references": [ "references": [
{
"path": "../ide-desktop/lib/dashboard/src/authentication/tsconfig.json"
},
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"
}, },

View File

@ -1,8 +1,16 @@
{ {
"extends": "@tsconfig/node18/tsconfig.json", "extends": "@tsconfig/node18/tsconfig.json",
"include": ["vite.config.*", "vitest.config.*", "playwright.config.*"], "include": [
"vite.config.*",
"vitest.config.*",
"playwright.config.*",
"eslint.config.js",
"ydoc-server/**/*",
"shared/**/*",
"e2e/**/*",
"node.env.d.ts"
],
"compilerOptions": { "compilerOptions": {
"composite": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"types": ["node"] "types": ["node"]

View File

@ -5,5 +5,10 @@
"composite": true, "composite": true,
"lib": [], "lib": [],
"types": ["node", "jsdom", "vitest/importMeta"] "types": ["node", "jsdom", "vitest/importMeta"]
} },
"references": [
{
"path": "../ide-desktop/lib/dashboard/tsconfig.json"
}
]
} }

View File

@ -1,47 +1,55 @@
import { fileURLToPath } from 'node:url'
import { defineConfig, Plugin } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
import postcssNesting from 'postcss-nesting' import postcssNesting from 'postcss-nesting'
import { WebSocketServer } from 'ws' import tailwindcss from 'tailwindcss'
import tailwindcssNesting from 'tailwindcss/nesting'
import { defineConfig, Plugin } from 'vite'
import topLevelAwait from 'vite-plugin-top-level-await' import topLevelAwait from 'vite-plugin-top-level-await'
import * as tailwindConfig from '../ide-desktop/lib/dashboard/tailwind.config'
import { createGatewayServer } from './ydoc-server'
const projectManagerUrl = 'ws://127.0.0.1:30535'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue(), yWebsocketServer(), topLevelAwait()], cacheDir: '../../node_modules/.cache/vite',
plugins: [vue(), gatewayServer(), topLevelAwait()],
optimizeDeps: {
entries: 'index.html',
},
resolve: { resolve: {
alias: { alias: {
shared: fileURLToPath(new URL('./shared', import.meta.url)), shared: fileURLToPath(new URL('./shared', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
// workaround for @open-rpc/client-js bug: https://github.com/open-rpc/client-js/issues/310
events: 'shared/event.ts',
}, },
}, },
define: {
REDIRECT_OVERRIDE: JSON.stringify('http://localhost:8080'),
PROJECT_MANAGER_URL: JSON.stringify(projectManagerUrl),
global: 'window',
IS_DEV_MODE: JSON.stringify(process.env.NODE_ENV !== 'production'),
},
assetsInclude: ['**/*.yaml', '**/*.svg'],
css: { css: {
postcss: { postcss: {
plugins: [postcssNesting], plugins: [tailwindcssNesting(postcssNesting()), tailwindcss({ config: tailwindConfig })],
}, },
}, },
build: {
// dashboard chunk size is larger than the default warning limit
chunkSizeWarningLimit: 700,
},
}) })
const roomNameRegex = /^[a-z0-9-]+$/i function gatewayServer(): Plugin {
function yWebsocketServer(): Plugin {
return { return {
name: 'y-websocket-server', name: 'gateway-server',
configureServer(server) { configureServer(server) {
if (server.httpServer == null) return if (server.httpServer == null) return
const { setupWSConnection } = require('./node_modules/y-websocket/bin/utils')
const wss = new WebSocketServer({ noServer: true }) createGatewayServer(server.httpServer)
wss.on('connection', setupWSConnection)
server.httpServer.on('upgrade', (request, socket, head) => {
if (request.url != null && request.url.startsWith('/room/')) {
const docName = request.url.slice(6)
if (roomNameRegex.test(docName)) {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request, { docName })
})
}
}
})
}, },
} }
} }

View File

@ -1,5 +1,5 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config' import viteConfig from './vite.config'
export default mergeConfig( export default mergeConfig(
@ -7,7 +7,7 @@ export default mergeConfig(
defineConfig({ defineConfig({
test: { test: {
environment: 'jsdom', environment: 'jsdom',
includeSource: ['./src/**/*.{ts,vue}'], includeSource: ['./{src,shared}/**/*.{ts,vue}'],
exclude: [...configDefaults.exclude, 'e2e/*'], exclude: [...configDefaults.exclude, 'e2e/*'],
root: fileURLToPath(new URL('./', import.meta.url)), root: fileURLToPath(new URL('./', import.meta.url)),
}, },

View File

@ -0,0 +1,83 @@
/**
* @file An entry point for the Yjs gateway server. The gateway server is a WebSocket server that
* synchronizes document requests and updates between language server and clients connected to the
* Yjs document mesh. It also serves as a central point for synchronizing document data and
* awareness updates between clients.
*
* Currently, this server is being run automatically in background as part of the vite development
* server. It is not yet deployed to any other environment.
*/
import { Server } from 'http'
import { IncomingMessage } from 'node:http'
import { parse } from 'url'
import { WebSocket, WebSocketServer } from 'ws'
import { setupGatewayClient } from './ydoc'
type ConnectionData = {
lsUrl: string
doc: string
user: string
}
export function createGatewayServer(httpServer: Server) {
const wss = new WebSocketServer({ noServer: true })
wss.on('connection', (ws: WebSocket, request: IncomingMessage, data: ConnectionData) => {
ws.on('error', onWebSocketError)
setupGatewayClient(ws, data.lsUrl, data.doc)
})
httpServer.on('upgrade', (request, socket, head) => {
socket.on('error', onHttpSocketError)
authenticate(request, function next(err, data) {
if (err != null) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
socket.destroy()
return
}
socket.removeListener('error', onHttpSocketError)
if (data != null) {
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request, data)
})
}
})
})
}
function onWebSocketError(err: Error) {
console.log('WebSocket error:', err)
}
function onHttpSocketError(err: Error) {
console.log('HTTP socket error:', err)
}
function authenticate(
request: IncomingMessage,
callback: (err: Error | null, authData: ConnectionData | null) => void,
) {
// FIXME: Stub. We don't implement authentication for now. Need to be implemented in combination
// with the language server.
const user = 'mock-user'
if (request.url == null) return callback(null, null)
const { pathname, query } = parse(request.url, true)
if (pathname == null) return callback(null, null)
const doc = docName(pathname)
const lsUrl = query.ls
const data = doc != null && typeof lsUrl === 'string' ? { lsUrl, doc, user } : null
callback(null, data)
}
const docNameRegex = /^[a-z0-9/-]+$/i
function docName(pathname: string) {
const prefix = '/project/'
if (pathname != null && pathname.startsWith(prefix)) {
const docName = pathname.slice(prefix.length)
if (docNameRegex.test(docName)) {
return docName
}
}
return null
}

View File

@ -0,0 +1,266 @@
import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
import * as map from 'lib0/map'
import { createMutex } from 'lib0/mutex'
import * as random from 'lib0/random'
import * as Y from 'yjs'
import { Emitter } from '../shared/event'
import { LanguageServer } from '../shared/languageServer'
import { Checksum, Path, response } from '../shared/languageServerTypes'
import { DistributedModule, DistributedProject, NodeMetadata, Uuid } from '../shared/yjsModel'
import { WSSharedDoc } from './ydoc'
const sessions = new Map<string, LanguageServerSession>()
type Events = {
error: [error: Error]
}
export class LanguageServerSession extends Emitter<Events> {
clientId: Uuid
indexDoc: WSSharedDoc
docs: Map<string, WSSharedDoc>
retainCount: number
url: string
client: Client
ls: LanguageServer
model: DistributedProject
projectRootId: Uuid | null
authoritativeModules: Map<string, ModulePersistence>
constructor(url: string) {
super()
this.clientId = random.uuidv4() as Uuid
this.docs = new Map()
this.retainCount = 0
this.url = url
const transport = new WebSocketTransport(url)
const requestManager = new RequestManager([transport])
this.client = new Client(requestManager)
transport.connection.on('error', (error) => this.emit('error', [error]))
this.ls = new LanguageServer(this.client)
this.indexDoc = new WSSharedDoc()
this.docs.set('index', this.indexDoc)
this.model = new DistributedProject(this.indexDoc.doc)
this.projectRootId = null
this.readInitialState()
this.authoritativeModules = new Map()
this.indexDoc.doc.on('subdocs', (subdocs: { loaded: Set<Y.Doc> }) => {
for (const doc of subdocs.loaded) {
const name = this.model.findModuleByDocId(doc.guid)
if (name == null) continue
const persistence = this.authoritativeModules.get(name)
if (persistence == null) continue
persistence.load()
}
})
this.model.modules.observe((event, transaction) => {
if (transaction.origin === this) return
const changes = event.changes
console.log('index change', changes.delta)
// let index = 0
// const indicesToDelete: number[] = []
// for (const op of delta) {
// if (op.insert != null) {
// const mod = this.model.openUnloadedModule(index)
// if (mod == null || !this.isAuthoritative(mod)) {
// console.log(`index ${index} not authoritative, deleting`)
// indicesToDelete.push(index - indicesToDelete.length)
// }
// index++
// } else if (op.delete != null) {
// console.error('TODO: Module delete')
// } else if (op.retain != null) {
// index += op.retain
// }
// }
// this.indexDoc.doc.transact(() => {
// for (const index of indicesToDelete) {
// this.model.modules.delete(index)
// }
// }, this)
// console.log('index update', this.model.moduleNames())
})
this.ls.addEventListener('file/event', (event) => {
console.log('file/event', event)
})
}
private assertProjectRoot(): asserts this is { projectRootId: Uuid } {
if (this.projectRootId == null) throw new Error('Missing project root')
}
private async readInitialState() {
try {
const { contentRoots } = await this.ls.initProtocolConnection(this.clientId)
const projectRoot = contentRoots.find((root) => root.type === 'Project') ?? null
if (projectRoot == null) throw new Error('Missing project root')
this.projectRootId = projectRoot.id
await this.ls.acquireReceivesTreeUpdates({ rootId: this.projectRootId, segments: [] })
const srcFiles = await this.scanSrcFiles()
await Promise.all(
this.indexDoc.doc.transact(() => {
return srcFiles.map((file) => this.getModuleModel(pushPathSegment(file.path, file.name)))
}, this),
)
} catch (error) {
console.error('LS Initialization failed:', error)
if (error instanceof Error) {
this.emit('error', [error])
}
return
}
console.log('LS connection initialized.')
}
async scanSrcFiles() {
this.assertProjectRoot()
const srcModules = await this.ls.listFiles({ rootId: this.projectRootId, segments: ['src'] })
return srcModules.paths.filter((file) => file.type === 'File' && file.name.endsWith('.enso'))
}
getModuleModel(path: Path): ModulePersistence {
const name = pathToModuleName(path)
return map.setIfUndefined(this.authoritativeModules, name, () => {
const wsDoc = new WSSharedDoc()
this.docs.set(wsDoc.doc.guid, wsDoc)
const model = this.model.createUnloadedModule(name, wsDoc.doc)
const mod = new ModulePersistence(model, path, this.ls)
mod.once('removed', () => {
const index = this.model.findModuleByDocId(wsDoc.doc.guid)
this.authoritativeModules.delete(name)
if (index != null) {
this.model.deleteModule(index)
}
})
return mod
})
}
static get(url: string): LanguageServerSession {
const session = map.setIfUndefined(sessions, url, () => new LanguageServerSession(url))
session.retain()
return session
}
retain() {
this.retainCount++
}
release() {
this.retainCount--
if (this.retainCount === 0) {
this.model.doc.destroy()
this.ls.dispose()
sessions.delete(this.url)
}
}
getYDoc(guid: string): WSSharedDoc | undefined {
return this.docs.get(guid)
}
}
const pathToModuleName = (path: Path): string => {
if (path.segments[0] === 'src') {
return path.segments.slice(1).join('/')
} else {
return '//' + path.segments.join('/')
}
}
const pushPathSegment = (path: Path, segment: string): Path => {
return {
rootId: path.rootId,
segments: [...path.segments, segment],
}
}
type Mutex = ReturnType<typeof createMutex>
class ModulePersistence extends Emitter<{ removed: [] }> {
ls: LanguageServer
model: DistributedModule
path: Path
currentVersion: Checksum | null
mux: Mutex
constructor(model: DistributedModule, path: Path, ls: LanguageServer) {
super()
this.ls = ls
this.model = model
this.path = path
this.currentVersion = null
this.mux = createMutex()
}
async load() {
let loaded: response.OpenTextFile
try {
loaded = await this.ls.openTextFile(this.path)
} catch (_) {
return this.dispose()
}
this.model.doc.transact(() => {
let code = loaded.content
const idMap = this.model.getIdMap()
try {
const metaTag = '#### METADATA ####'
const splitPoint = loaded.content.lastIndexOf(metaTag)
if (splitPoint < 0) throw new Error('Metadata not found')
code = loaded.content.slice(0, splitPoint)
const metadataString = loaded.content.slice(splitPoint + metaTag.length)
const metaLines = metadataString.trim().split('\n')
const idMapMeta = JSON.parse(metaLines[0])
const ideMeta = JSON.parse(metaLines[1])?.ide
const nodeMeta = ideMeta?.node
for (const [{ index, size }, id] of idMapMeta) {
const range = [index.value, index.value + size.value]
if (typeof range[0] !== 'number' || typeof range[1] !== 'number') {
console.error(`Invalid range for id ${id}:`, range)
continue
}
idMap.insertKnownId([index.value, index.value + size.value], id)
}
for (const [id, rawMeta] of Object.entries(nodeMeta ?? {})) {
if (typeof id !== 'string') continue
const meta = rawMeta as any
const formattedMeta: NodeMetadata = {
x: meta?.position?.vector?.[0] ?? 0,
y: meta?.position?.vector?.[1] ?? 0,
vis: meta?.visualization ?? undefined,
}
this.model.metadata.set(id, formattedMeta)
}
} catch (e) {
console.log('Metadata parse failed:', e)
}
this.currentVersion = loaded.currentVersion
this.model.contents.delete(0, this.model.contents.length)
this.model.contents.insert(0, code)
idMap.finishAndSynchronize()
}, this)
}
/** A file was removed on the LS side. */
dispose() {
this.model.doc.destroy()
this.emit('removed', [])
}
handleLsUpdate() {
this.mux(() => {})
}
handleDocUpdate() {
this.mux(() => {})
}
}

View File

@ -0,0 +1,235 @@
import {
applyAwarenessUpdate,
Awareness,
encodeAwarenessUpdate,
removeAwarenessStates,
} from 'y-protocols/awareness'
import { readSyncMessage, writeSyncStep1, writeUpdate } from 'y-protocols/sync'
import * as Y from 'yjs'
import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding'
import { WebSocket } from 'ws'
import { Emitter } from '../shared/event'
import { LanguageServerSession } from './languageServerSession'
const pingTimeout = 30000
const messageSync = 0
const messageAwareness = 1
interface AwarenessUpdate {
added: number[]
updated: number[]
removed: number[]
}
type ConnectionId = YjsConnection | string
/**
* A Yjs document that is shared over multiple websocket connections.
*/
export class WSSharedDoc {
doc: Y.Doc
/**
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn
* is closed.
*/
conns: Map<ConnectionId, Set<number>>
awareness: Awareness
constructor(gc = true) {
this.doc = new Y.Doc({ gc })
// this.name = name
this.conns = new Map()
this.awareness = new Awareness(this.doc)
this.awareness.setLocalState(null)
this.awareness.on(
'update',
({ added, updated, removed }: AwarenessUpdate, conn: ConnectionId | null) => {
const changedClients = added.concat(updated, removed)
if (conn !== null) {
const connControlledIDs = this.conns.get(conn)
if (connControlledIDs !== undefined) {
added.forEach((clientID) => {
connControlledIDs.add(clientID)
})
removed.forEach((clientID) => {
connControlledIDs.delete(clientID)
})
}
}
// broadcast awareness update
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(encoder, encodeAwarenessUpdate(this.awareness, changedClients))
this.broadcast(encoding.toUint8Array(encoder))
},
)
this.doc.on('update', (update, origin) => this.updateHandler(update, origin))
}
broadcast(message: Uint8Array) {
this.conns.forEach((_, conn) => {
if (typeof conn !== 'string') {
conn.send(message)
}
})
}
updateHandler(update: Uint8Array, _origin: any) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
writeUpdate(encoder, update)
this.broadcast(encoding.toUint8Array(encoder))
}
}
/**
* Handle servicing incoming websocket connection listening for given document updates.
* @param ws The newly connected websocket requesting Yjs document synchronization
* @param lsUrl Address of the language server to connect to. Each unique language server address
* will be assigned its own `DistributedProject` instance with a unique namespace of Yjs documents.
* @param docName The name of the document to synchronize. When the document name is `index`, the
* document is considered to be the root document of the `DistributedProject` data model.
*/
export function setupGatewayClient(ws: WebSocket, lsUrl: string, docName: string) {
const lsSession = LanguageServerSession.get(lsUrl)
const wsDoc = lsSession.getYDoc(docName)
if (wsDoc == null) {
console.log(`Document ${docName} not found in language server session ${lsUrl}`)
ws.close()
return
}
const connection = new YjsConnection(ws, wsDoc)
const doClose = () => connection.close()
lsSession.on('error', doClose)
connection.on('close', () => {
lsSession.off('error', doClose)
lsSession.release()
})
}
class YjsConnection extends Emitter<{ close: [] }> {
ws: WebSocket
wsDoc: WSSharedDoc
constructor(ws: WebSocket, wsDoc: WSSharedDoc) {
super()
this.ws = ws
this.wsDoc = wsDoc
const isLoaded = wsDoc.conns.size > 0
wsDoc.conns.set(this, new Set())
ws.binaryType = 'arraybuffer'
ws.on('message', (message: ArrayBuffer) => this.messageListener(new Uint8Array(message)))
ws.on('close', () => this.close())
if (!isLoaded) {
wsDoc.doc.load()
}
this.initPing()
this.sendSyncMessage()
}
private initPing() {
// Check if connection is still alive
let pongReceived = true
const pingInterval = setInterval(() => {
if (!pongReceived) {
if (this.wsDoc.conns.has(this)) {
this.close()
}
clearInterval(pingInterval)
} else if (this.wsDoc.conns.has(this)) {
pongReceived = false
try {
this.ws.ping()
} catch (e) {
this.close()
clearInterval(pingInterval)
}
}
}, pingTimeout)
this.ws.on('close', () => clearInterval(pingInterval))
this.ws.on('pong', () => {
pongReceived = true
})
}
sendSyncMessage() {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageSync)
writeSyncStep1(encoder, this.wsDoc.doc)
this.send(encoding.toUint8Array(encoder))
const awarenessStates = this.wsDoc.awareness.getStates()
if (awarenessStates.size > 0) {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, messageAwareness)
encoding.writeVarUint8Array(
encoder,
encodeAwarenessUpdate(this.wsDoc.awareness, Array.from(awarenessStates.keys())),
)
this.send(encoding.toUint8Array(encoder))
}
}
send(message: Uint8Array) {
if (this.ws.readyState !== WebSocket.CONNECTING && this.ws.readyState !== WebSocket.OPEN) {
this.close()
}
try {
this.ws.send(message, (error) => {
if (error != null) {
this.close()
}
})
} catch (e) {
this.close()
}
}
messageListener(message: Uint8Array) {
try {
const encoder = encoding.createEncoder()
const decoder = decoding.createDecoder(message)
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case messageSync:
encoding.writeVarUint(encoder, messageSync)
readSyncMessage(decoder, encoder, this.wsDoc.doc, this)
// If the `encoder` only contains the type of reply message and no
// message, there is no need to send the message. When `encoder` only
// contains the type of reply, its length is 1.
if (encoding.length(encoder) > 1) {
this.send(encoding.toUint8Array(encoder))
}
break
case messageAwareness: {
const update = decoding.readVarUint8Array(decoder)
applyAwarenessUpdate(this.wsDoc.awareness, update, this)
break
}
}
} catch (err) {
console.error(err)
this.wsDoc.doc.emit('error', [err])
}
}
close() {
const controlledIds = this.wsDoc.conns.get(this)
this.wsDoc.conns.delete(this)
if (controlledIds != null) {
removeAwarenessStates(this.wsDoc.awareness, Array.from(controlledIds), null)
}
this.ws.close()
this.emit('close', [])
if (this.wsDoc.conns.size === 0) {
this.wsDoc.doc.emit('unload', [])
}
}
}

View File

@ -8,7 +8,6 @@
import * as childProcess from 'node:child_process' import * as childProcess from 'node:child_process'
import * as fs from 'node:fs/promises' import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import * as electronBuilder from 'electron-builder' import * as electronBuilder from 'electron-builder'
import * as electronNotarize from 'electron-notarize' import * as electronNotarize from 'electron-notarize'
@ -19,9 +18,10 @@ import * as common from 'enso-common'
import * as fileAssociations from './file-associations' import * as fileAssociations from './file-associations'
import * as paths from './paths' import * as paths from './paths'
import computeHashes from './tasks/computeHashes.mjs'
import signArchivesMacOs from './tasks/signArchivesMacOs' import signArchivesMacOs from './tasks/signArchivesMacOs'
import BUILD_INFO from '../../build.json' assert { type: 'json' } import BUILD_INFO from '../../../../build.json' assert { type: 'json' }
// ============= // =============
// === Types === // === Types ===
@ -213,8 +213,7 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
// https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ // https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
sign: false, sign: false,
}, },
afterAllArtifactBuild: path.join('tasks', 'computeHashes.cjs'), afterAllArtifactBuild: computeHashes,
afterPack: ctx => { afterPack: ctx => {
if (passedArgs.platform === electronBuilder.Platform.MAC) { if (passedArgs.platform === electronBuilder.Platform.MAC) {
// Make the subtree writable, so we can sign the binaries. // Make the subtree writable, so we can sign the binaries.

View File

@ -20,7 +20,7 @@
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/opener": "^1.4.0", "@types/opener": "^1.4.0",
"chalk": "^5.2.0", "chalk": "^5.2.0",
"create-servers": "^3.2.0", "create-servers": "3.2.0",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"opener": "^1.5.2", "opener": "^1.5.2",
@ -38,7 +38,7 @@
"electron-builder": "^22.14.13", "electron-builder": "^22.14.13",
"electron-notarize": "1.2.2", "electron-notarize": "1.2.2",
"enso-common": "^1.0.0", "enso-common": "^1.0.0",
"esbuild": "^0.17.15", "esbuild": "^0.19.3",
"fast-glob": "^3.2.12", "fast-glob": "^3.2.12",
"portfinder": "^1.0.32", "portfinder": "^1.0.32",
"tsx": "^3.12.6" "tsx": "^3.12.6"
@ -51,7 +51,6 @@
}, },
"scripts": { "scripts": {
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "npx --yes eslint src",
"start": "tsx start.ts", "start": "tsx start.ts",
"build": "tsx bundle.ts", "build": "tsx bundle.ts",
"dist": "tsx dist.ts", "dist": "tsx dist.ts",

View File

@ -26,7 +26,10 @@ const DEFAULT_PORT = 8080
export class WindowSize { export class WindowSize {
static separator = 'x' static separator = 'x'
/** Create a new {@link WindowSize}. */ /** Create a new {@link WindowSize}. */
constructor(public width: number, public height: number) {} constructor(
public width: number,
public height: number
) {}
/** Constructor of the default window size. */ /** Constructor of the default window size. */
static default(): WindowSize { static default(): WindowSize {

View File

@ -3,15 +3,15 @@
import chalk from 'chalk' import chalk from 'chalk'
import stringLength from 'string-length' import stringLength from 'string-length'
import yargs from 'yargs/yargs' // eslint-disable-next-line no-restricted-syntax
import yargsModule from 'yargs' import yargs, { Options } from 'yargs'
import * as contentConfig from 'enso-content-config' import * as contentConfig from 'enso-content-config'
import * as config from 'config' import * as config from 'config'
import * as fileAssociations from 'file-associations' import * as fileAssociations from 'file-associations'
import * as naming from 'naming' import * as naming from 'naming'
import BUILD_INFO from '../../../../build.json' assert { type: 'json' } import BUILD_INFO from '../../../../../../build.json' assert { type: 'json' }
const logger = contentConfig.logger const logger = contentConfig.logger
@ -206,7 +206,10 @@ function wordWrap(str: string, width: number): string[] {
/** Represents a command line option to be passed to the Chrome instance powering Electron. */ /** Represents a command line option to be passed to the Chrome instance powering Electron. */
export class ChromeOption { export class ChromeOption {
/** Create a {@link ChromeOption}. */ /** Create a {@link ChromeOption}. */
constructor(public name: string, public value?: string) {} constructor(
public name: string,
public value?: string
) {}
/** Return the option as it would appear on the command line. */ /** Return the option as it would appear on the command line. */
display(): string { display(): string {
@ -275,20 +278,18 @@ function argvAndChromeOptions(processArgs: string[]): ArgvAndChromeOptions {
export function parseArgs(clientArgs: string[] = fileAssociations.CLIENT_ARGUMENTS) { export function parseArgs(clientArgs: string[] = fileAssociations.CLIENT_ARGUMENTS) {
const args = config.CONFIG const args = config.CONFIG
const { argv, chromeOptions } = argvAndChromeOptions(fixArgvNoPrefix(clientArgs)) const { argv, chromeOptions } = argvAndChromeOptions(fixArgvNoPrefix(clientArgs))
const yargsOptions = args const yargsOptions = args.optionsRecursive().reduce((opts: Record<string, Options>, option) => {
.optionsRecursive() opts[naming.camelToKebabCase(option.qualifiedName())] = {
.reduce((opts: Record<string, yargsModule.Options>, option) => { ...option,
opts[naming.camelToKebabCase(option.qualifiedName())] = { requiresArg: ['string', 'array'].includes(option.type),
...option, default: null,
requiresArg: ['string', 'array'].includes(option.type), // Required because yargs defines `defaultDescription`
default: null, // as `string | undefined`, not `string | null`.
// Required because yargs defines `defaultDescription` // eslint-disable-next-line no-restricted-syntax
// as `string | undefined`, not `string | null`. defaultDescription: option.defaultDescription ?? undefined,
// eslint-disable-next-line no-restricted-syntax }
defaultDescription: option.defaultDescription ?? undefined, return opts
} }, {})
return opts
}, {})
const optParser = yargs() const optParser = yargs()
.version(false) .version(false)

View File

@ -1,6 +1,6 @@
/** @file Application debug information. */ /** @file Application debug information. */
import BUILD_INFO from '../../../build.json' assert { type: 'json' } import BUILD_INFO from '../../../../../build.json' assert { type: 'json' }
// ================= // =================
// === Constants === // === Constants ===

View File

@ -9,11 +9,11 @@
import * as fsSync from 'node:fs' import * as fsSync from 'node:fs'
import * as pathModule from 'node:path' import * as pathModule from 'node:path'
import * as linkedDist from 'ensogl-runner/src/runner'
import * as contentConfig from 'enso-content-config' import * as contentConfig from 'enso-content-config'
import * as paths from 'paths' import * as paths from 'paths'
import * as linkedDist from '../../../../../target/ensogl-pack/linked-dist'
// ================ // ================
// === Log File === // === Log File ===
// ================ // ================

View File

@ -1,11 +1,8 @@
/** @file Definition of hash computing functions. */ /** @file Definition of hash computing functions. */
// Eslint is not (and should not be) set up to recognize CommonJS imports. import * as cryptoModule from 'node:crypto'
/* eslint-disable no-restricted-syntax */ import * as fs from 'node:fs'
const cryptoModule = require('crypto') import * as pathModule from 'node:path'
const fs = require('fs')
const pathModule = require('path')
/* eslint-enable no-restricted-syntax */
// ================= // =================
// === Constants === // === Constants ===
@ -63,8 +60,10 @@ async function writeFileChecksum(path, type) {
// ================ // ================
/** Generates checksums for all build artifacts. /** Generates checksums for all build artifacts.
* @param {import('electron-builder').BuildResult} context - Build information. */ * @param {import('electron-builder').BuildResult} context - Build information.
exports.default = async function (context) { * @returns {Promise<string[]>} afterAllArtifactBuild hook result.
*/
export default async function (context) {
// `context` is BuildResult, see // `context` is BuildResult, see
// https://www.electron.build/configuration/configuration.html#buildresult // https://www.electron.build/configuration/configuration.html#buildresult
for (const file of context.artifactPaths) { for (const file of context.artifactPaths) {

View File

@ -4,5 +4,12 @@
"baseUrl": "./src", "baseUrl": "./src",
"esModuleInterop": true "esModuleInterop": true
}, },
"include": ["../types", "."] "include": [
".",
"../content",
"../dashboard",
"../types",
"../../utils.ts",
"../../../../build.json"
]
} }

View File

@ -86,6 +86,7 @@ const ALL_BUNDLES_READY = new Promise<Watches>((resolve, reject) => {
}, },
}) })
dashboardOpts.outdir = path.resolve(IDE_DIR_PATH, 'assets') dashboardOpts.outdir = path.resolve(IDE_DIR_PATH, 'assets')
dashboardOpts.write = false
const dashboardBuilder = await esbuild.context(dashboardOpts) const dashboardBuilder = await esbuild.context(dashboardOpts)
const dashboard = await dashboardBuilder.rebuild() const dashboard = await dashboardBuilder.rebuild()
console.log('Result of dashboard bundling: ', dashboard) console.log('Result of dashboard bundling: ', dashboard)

View File

@ -2,8 +2,8 @@
import * as semver from 'semver' import * as semver from 'semver'
import * as linkedDist from '../../../../../target/ensogl-pack/linked-dist' import * as linkedDist from 'ensogl-runner/src/runner'
import BUILD_INFO from '../../../build.json' assert { type: 'json' } import BUILD_INFO from '../../../../../build.json' assert { type: 'json' }
// Aliases with the same name as the original. // Aliases with the same name as the original.
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax

View File

@ -1,4 +1,4 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": ["../types", "."] "include": ["../types", "../../../../build.json", ".", "src/config.json"]
} }

View File

@ -16,13 +16,12 @@ import * as url from 'node:url'
import * as esbuild from 'esbuild' import * as esbuild from 'esbuild'
import * as esbuildPluginNodeGlobals from '@esbuild-plugins/node-globals-polyfill' import * as esbuildPluginNodeGlobals from '@esbuild-plugins/node-globals-polyfill'
import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill' import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill'
import esbuildPluginAlias from 'esbuild-plugin-alias'
import esbuildPluginCopyDirectories from 'esbuild-plugin-copy-directories' import esbuildPluginCopyDirectories from 'esbuild-plugin-copy-directories'
import esbuildPluginTime from 'esbuild-plugin-time' import esbuildPluginTime from 'esbuild-plugin-time'
import esbuildPluginYaml from 'esbuild-plugin-yaml' import esbuildPluginYaml from 'esbuild-plugin-yaml'
import * as utils from '../../utils' import * as utils from '../../utils'
import BUILD_INFO from '../../build.json' assert { type: 'json' } import BUILD_INFO from '../../../../build.json' assert { type: 'json' }
// ================= // =================
// === Constants === // === Constants ===
@ -54,8 +53,6 @@ export interface Arguments extends PassthroughArguments {
assetsPath: string assetsPath: string
/** Path where bundled files are output. */ /** Path where bundled files are output. */
outputPath: string outputPath: string
/** The main JS bundle to load WASM and JS wasm-pack bundles. */
ensoglAppPath: string
} }
/** Get arguments from the environment. */ /** Get arguments from the environment. */
@ -63,8 +60,7 @@ export function argumentsFromEnv(passthroughArguments: PassthroughArguments): Ar
const wasmArtifacts = utils.requireEnv('ENSO_BUILD_GUI_WASM_ARTIFACTS') const wasmArtifacts = utils.requireEnv('ENSO_BUILD_GUI_WASM_ARTIFACTS')
const assetsPath = utils.requireEnv('ENSO_BUILD_GUI_ASSETS') const assetsPath = utils.requireEnv('ENSO_BUILD_GUI_ASSETS')
const outputPath = pathModule.resolve(utils.requireEnv('ENSO_BUILD_GUI'), 'assets') const outputPath = pathModule.resolve(utils.requireEnv('ENSO_BUILD_GUI'), 'assets')
const ensoglAppPath = utils.requireEnv('ENSO_BUILD_GUI_ENSOGL_APP') return { ...passthroughArguments, wasmArtifacts, assetsPath, outputPath }
return { ...passthroughArguments, wasmArtifacts, assetsPath, outputPath, ensoglAppPath }
} }
// =================== // ===================
@ -89,7 +85,6 @@ function git(command: string): string {
export function bundlerOptions(args: Arguments) { export function bundlerOptions(args: Arguments) {
const { const {
outputPath, outputPath,
ensoglAppPath,
wasmArtifacts, wasmArtifacts,
assetsPath, assetsPath,
devMode, devMode,
@ -156,7 +151,6 @@ export function bundlerOptions(args: Arguments) {
esbuildPluginYaml.yamlPlugin({}), esbuildPluginYaml.yamlPlugin({}),
esbuildPluginNodeModules.NodeModulesPolyfillPlugin(), esbuildPluginNodeModules.NodeModulesPolyfillPlugin(),
esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }), esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }),
esbuildPluginAlias({ ensogl_app: ensoglAppPath }),
esbuildPluginTime(), esbuildPluginTime(),
], ],
define: { define: {

View File

@ -16,7 +16,7 @@
}, },
"scripts": { "scripts": {
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "npx --yes eslint src", "lint": "eslint src",
"build": "tsx bundle.ts", "build": "tsx bundle.ts",
"watch": "tsx watch.ts", "watch": "tsx watch.ts",
"start": "tsx start.ts" "start": "tsx start.ts"
@ -29,27 +29,26 @@
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@eslint/js": "^8.36.0", "@eslint/js": "^8.49.0",
"@types/connect": "^3.4.35", "@types/connect": "^3.4.35",
"@types/morgan": "^1.9.4", "@types/morgan": "^1.9.4",
"@types/serve-static": "^1.15.1", "@types/serve-static": "^1.15.1",
"@types/sharp": "^0.31.1", "@types/sharp": "^0.31.1",
"@types/to-ico": "^1.1.1", "@types/to-ico": "^1.1.1",
"@types/ws": "^8.5.4", "@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^5.55.0", "@typescript-eslint/parser": "^6.7.2",
"enso-authentication": "^1.0.0", "enso-authentication": "^1.0.0",
"esbuild": "^0.17.15", "esbuild": "^0.19.3",
"esbuild-plugin-alias": "^0.2.1",
"esbuild-plugin-copy-directories": "^1.0.0", "esbuild-plugin-copy-directories": "^1.0.0",
"esbuild-plugin-time": "^1.0.0", "esbuild-plugin-time": "^1.0.0",
"esbuild-plugin-yaml": "^0.0.1", "esbuild-plugin-yaml": "^0.0.1",
"eslint": "^8.36.0", "eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^40.0.2", "eslint-plugin-jsdoc": "^46.8.1",
"globals": "^13.20.0", "globals": "^13.20.0",
"portfinder": "^1.0.32", "portfinder": "^1.0.32",
"tsx": "^3.12.6", "tsx": "^3.12.6",
"typescript": "^4.9.3" "typescript": "~5.2.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/darwin-x64": "^0.17.15", "@esbuild/darwin-x64": "^0.17.15",

View File

@ -4,7 +4,7 @@
via a symlink. This is temporary, while the `content` and `dashboard` have separate entrypoints via a symlink. This is temporary, while the `content` and `dashboard` have separate entrypoints
for cloud and desktop. Once they are merged, the symlink must be removed. for cloud and desktop. Once they are merged, the symlink must be removed.
--> -->
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@ -5,12 +5,12 @@
import * as semver from 'semver' import * as semver from 'semver'
import * as toastify from 'react-toastify' import * as toastify from 'react-toastify'
import * as app from 'ensogl-runner/src/runner'
import * as common from 'enso-common' import * as common from 'enso-common'
import * as contentConfig from 'enso-content-config' import * as contentConfig from 'enso-content-config'
import * as dashboard from 'enso-authentication' import * as dashboard from 'enso-authentication'
import * as detect from 'enso-common/src/detect' import * as detect from 'enso-common/src/detect'
import * as app from '../../../../../target/ensogl-pack/linked-dist'
import * as remoteLog from './remoteLog' import * as remoteLog from './remoteLog'
import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' } import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' }
@ -141,7 +141,7 @@ function displayDeprecatedVersionDialog() {
// ======================== // ========================
/** Nested configuration options with `string` values. */ /** Nested configuration options with `string` values. */
interface StringConfig { export interface StringConfig {
[key: string]: StringConfig | string [key: string]: StringConfig | string
} }
@ -150,9 +150,7 @@ interface AuthenticationConfig {
projectManagerUrl: string | null projectManagerUrl: string | null
isInAuthenticationFlow: boolean isInAuthenticationFlow: boolean
shouldUseAuthentication: boolean shouldUseAuthentication: boolean
shouldUseNewDashboard: boolean
initialProjectName: string | null initialProjectName: string | null
inputConfig: StringConfig | null
} }
/** Contains the entrypoint into the IDE. */ /** Contains the entrypoint into the IDE. */
@ -258,8 +256,6 @@ class Main implements AppRunner {
} }
if (parseOk) { if (parseOk) {
const shouldUseAuthentication = configOptions.options.authentication.value const shouldUseAuthentication = configOptions.options.authentication.value
const shouldUseNewDashboard =
configOptions.groups.featurePreview.options.newDashboard.value
const isOpeningMainEntryPoint = const isOpeningMainEntryPoint =
configOptions.groups.startup.options.entry.value === configOptions.groups.startup.options.entry.value ===
configOptions.groups.startup.options.entry.default configOptions.groups.startup.options.entry.default
@ -275,14 +271,12 @@ class Main implements AppRunner {
url.searchParams.delete('startup.project') url.searchParams.delete('startup.project')
history.replaceState(null, '', url.toString()) history.replaceState(null, '', url.toString())
} }
if ((shouldUseAuthentication || shouldUseNewDashboard) && isOpeningMainEntryPoint) { if (shouldUseAuthentication && isOpeningMainEntryPoint) {
this.runAuthentication({ this.runAuthentication({
isInAuthenticationFlow, isInAuthenticationFlow,
projectManagerUrl, projectManagerUrl,
shouldUseAuthentication, shouldUseAuthentication,
shouldUseNewDashboard,
initialProjectName, initialProjectName,
inputConfig: inputConfig ?? null,
}) })
} else { } else {
void this.runApp(inputConfig ?? null, null) void this.runApp(inputConfig ?? null, null)
@ -292,6 +286,12 @@ class Main implements AppRunner {
/** Begins the authentication UI flow. */ /** Begins the authentication UI flow. */
runAuthentication(config: AuthenticationConfig) { runAuthentication(config: AuthenticationConfig) {
const ideElement = document.getElementById('root')
if (ideElement) {
ideElement.style.top = '-100vh'
ideElement.style.display = 'fixed'
}
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/345 /** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/345
* `content` and `dashboard` packages **MUST BE MERGED INTO ONE**. The IDE * `content` and `dashboard` packages **MUST BE MERGED INTO ONE**. The IDE
* should only have one entry point. Right now, we have two. One for the cloud * should only have one entry point. Right now, we have two. One for the cloud
@ -308,9 +308,9 @@ class Main implements AppRunner {
supportsDeepLinks: SUPPORTS_DEEP_LINKS, supportsDeepLinks: SUPPORTS_DEEP_LINKS,
projectManagerUrl: config.projectManagerUrl, projectManagerUrl: config.projectManagerUrl,
isAuthenticationDisabled: !config.shouldUseAuthentication, isAuthenticationDisabled: !config.shouldUseAuthentication,
shouldShowDashboard: config.shouldUseNewDashboard, shouldShowDashboard: true,
initialProjectName: config.initialProjectName, initialProjectName: config.initialProjectName,
onAuthenticated: (accessToken: string | null) => { onAuthenticated: () => {
if (config.isInAuthenticationFlow) { if (config.isInAuthenticationFlow) {
const initialUrl = localStorage.getItem(INITIAL_URL_KEY) const initialUrl = localStorage.getItem(INITIAL_URL_KEY)
if (initialUrl != null) { if (initialUrl != null) {
@ -319,17 +319,6 @@ class Main implements AppRunner {
history.replaceState(null, '', initialUrl) history.replaceState(null, '', initialUrl)
} }
} }
if (!config.shouldUseNewDashboard) {
document.getElementById('enso-dashboard')?.remove()
const ideElement = document.getElementById('root')
if (ideElement) {
ideElement.style.top = ''
ideElement.style.display = ''
}
if (this.app == null) {
void this.runApp(config.inputConfig, accessToken)
}
}
}, },
}) })
} }

View File

@ -1,7 +1,7 @@
/** @file Defines the {@link RemoteLogger} class and {@link remoteLog} function for sending logs to a remote server. /** @file Defines the {@link RemoteLogger} class and {@link remoteLog} function for sending logs to a remote server.
* {@link RemoteLogger} provides a convenient way to manage remote logging with access token authorization. */ * {@link RemoteLogger} provides a convenient way to manage remote logging with access token authorization. */
import * as app from '../../../../../target/ensogl-pack/linked-dist' import * as app from 'ensogl-runner/src/runner'
import * as authConfig from '../../dashboard/src/authentication/src/config' import * as authConfig from '../../dashboard/src/authentication/src/config'
const logger = app.log.logger const logger = app.log.logger

View File

@ -1,4 +1,10 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": ["../types", "."] "include": [
"../types",
"../../utils.ts",
"../../../../build.json",
"../dashboard",
"."
]
} }

View File

@ -12,6 +12,7 @@ import * as bundler from './esbuild-config'
// ================= // =================
export const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url))) export const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
export const ANALYZE = process.argv.includes('--analyze')
// =============== // ===============
// === Bundler === // === Bundler ===
@ -33,8 +34,12 @@ async function bundle() {
path.resolve(THIS_PATH, 'src', 'index.html'), path.resolve(THIS_PATH, 'src', 'index.html'),
path.resolve(THIS_PATH, 'src', 'index.tsx') path.resolve(THIS_PATH, 'src', 'index.tsx')
) )
opts.metafile = ANALYZE
opts.loader['.html'] = 'copy' opts.loader['.html'] = 'copy'
await esbuild.build(opts) const result = await esbuild.build(opts)
if (result.metafile) {
console.log(await esbuild.analyzeMetafile(result.metafile))
}
return return
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -12,6 +12,7 @@ import * as url from 'node:url'
import * as esbuild from 'esbuild' import * as esbuild from 'esbuild'
import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill' import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill'
import esbuildPluginInlineImage from 'esbuild-plugin-inline-image'
import esbuildPluginTime from 'esbuild-plugin-time' import esbuildPluginTime from 'esbuild-plugin-time'
import esbuildPluginYaml from 'esbuild-plugin-yaml' import esbuildPluginYaml from 'esbuild-plugin-yaml'
@ -19,6 +20,7 @@ import postcss from 'postcss'
import tailwindcss from 'tailwindcss' import tailwindcss from 'tailwindcss'
import tailwindcssNesting from 'tailwindcss/nesting/index.js' import tailwindcssNesting from 'tailwindcss/nesting/index.js'
import * as tailwindConfig from './tailwind.config'
import * as utils from '../../utils' import * as utils from '../../utils'
// ================= // =================
@ -26,7 +28,6 @@ import * as utils from '../../utils'
// ================= // =================
const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url))) const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
const TAILWIND_CONFIG_PATH = path.resolve(THIS_PATH, 'tailwind.config.ts')
// ============================= // =============================
// === Environment variables === // === Environment variables ===
@ -51,25 +52,25 @@ export function argumentsFromEnv(): Arguments {
// ======================= // =======================
/** A plugin to process all CSS files with Tailwind CSS. */ /** A plugin to process all CSS files with Tailwind CSS. */
function esbuildPluginGenerateTailwind(): esbuild.Plugin { export function esbuildPluginGenerateTailwind(): esbuild.Plugin {
return { return {
name: 'enso-generate-tailwind', name: 'enso-generate-tailwind',
setup: build => { setup: build => {
const cssProcessor = postcss([ const cssProcessor = postcss([
tailwindcss({ tailwindcss({
config: TAILWIND_CONFIG_PATH, config: tailwindConfig,
}), }),
tailwindcssNesting(), tailwindcssNesting(),
]) ])
build.onLoad({ filter: /tailwind\.css$/ }, async loadArgs => { build.onLoad({ filter: /tailwind\.css$/ }, async loadArgs => {
console.log(`Processing CSS file '${loadArgs.path}'.`) // console.log(`Processing CSS file '${loadArgs.path}'.`)
const content = await fs.readFile(loadArgs.path, 'utf8') const content = await fs.readFile(loadArgs.path, 'utf8')
const result = await cssProcessor.process(content, { from: loadArgs.path }) const result = await cssProcessor.process(content, { from: loadArgs.path })
console.log(`Processed CSS file '${loadArgs.path}'.`) // console.log(`Processed CSS file '${loadArgs.path}'.`)
return { return {
contents: result.content, contents: result.content,
loader: 'css', loader: 'css',
watchFiles: [loadArgs.path, TAILWIND_CONFIG_PATH], watchFiles: [loadArgs.path],
} }
}) })
}, },
@ -93,19 +94,20 @@ export function bundlerOptions(args: Arguments) {
outdir: outputPath, outdir: outputPath,
outbase: 'src', outbase: 'src',
loader: { loader: {
// The CSS file needs to import a single SVG as a data URL.
// For `bundle.ts` and `watch.ts`, `index.js` also includes various SVG icons
// which need to be bundled.
// The `dataurl` loader replaces the import with the file, as a data URL. Using the
// `file` loader, which copies the file and replaces the import with the path,
// is an option, however this loader avoids adding extra files to the bundle.
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
'.svg': 'dataurl',
// The `file` loader copies the file, and replaces the import with the path to the file. // The `file` loader copies the file, and replaces the import with the path to the file.
'.png': 'file', '.png': 'file',
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
plugins: [ plugins: [
// The CSS file needs to import a single SVG as a data URL.
// For `bundle.ts` and `watch.ts`, `index.js` also includes various SVG icons
// which need to be bundled.
// Depending on file size, choose between `dataurl` and `file` loaders.
// The `dataurl` loader replaces the import with the file, as a data URL. Using the
// `file` loader, which copies the file and replaces the import with the path.
/* eslint-disable @typescript-eslint/naming-convention */
esbuildPluginInlineImage({ extensions: ['svg'] }),
esbuildPluginNodeModules.NodeModulesPolyfillPlugin(), esbuildPluginNodeModules.NodeModulesPolyfillPlugin(),
esbuildPluginTime(), esbuildPluginTime(),
// This is not strictly needed because the cloud frontend does not use // This is not strictly needed because the cloud frontend does not use
@ -125,7 +127,7 @@ export function bundlerOptions(args: Arguments) {
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
}, },
pure: ['assert'], pure: ['assert'],
sourcemap: trueBoolean, sourcemap: true,
minify: !devMode, minify: !devMode,
metafile: trueBoolean, metafile: trueBoolean,
format: 'esm', format: 'esm',

View File

@ -4,8 +4,8 @@
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"typecheck": "tsc --noEmit", "typecheck": "tsc",
"lint": "npx --yes eslint src", "lint": "eslint src",
"build": "tsx bundle.ts", "build": "tsx bundle.ts",
"watch": "tsx watch.ts", "watch": "tsx watch.ts",
"start": "tsx start.ts", "start": "tsx start.ts",
@ -13,10 +13,10 @@
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.0.15", "@heroicons/react": "^2.0.15",
"@types/node": "^16.18.11", "@types/node": "^18.17.5",
"@types/react": "^18.0.27", "@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"esbuild": "^0.17.15", "esbuild": "^0.19.3",
"esbuild-plugin-time": "^1.0.0", "esbuild-plugin-time": "^1.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -24,18 +24,19 @@
}, },
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@typescript-eslint/eslint-plugin": "^5.49.0", "@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^5.49.0", "@typescript-eslint/parser": "^6.7.2",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"enso-authentication": "^1.0.0", "enso-authentication": "^1.0.0",
"enso-chat": "git://github.com/enso-org/enso-bot", "enso-chat": "git://github.com/enso-org/enso-bot",
"enso-content": "^1.0.0", "enso-content": "^1.0.0",
"eslint": "^8.32.0", "eslint": "^8.49.0",
"eslint-plugin-jsdoc": "^39.6.8", "eslint-plugin-jsdoc": "^46.8.1",
"eslint-plugin-react": "^7.32.1", "eslint-plugin-react": "^7.32.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "^4.9.4" "typescript": "~5.2.2",
"tsx": "^3.12.6"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/darwin-x64": "^0.17.15", "@esbuild/darwin-x64": "^0.17.15",

View File

@ -5,15 +5,17 @@
"main": "./src/index.tsx", "main": "./src/index.tsx",
"exports": { "exports": {
".": "./src/index.tsx", ".": "./src/index.tsx",
"./src/platform": "./src/platform.ts" "./src/platform": "./src/platform.ts",
"./tailwind.css": "./tailwind.css"
}, },
"scripts": { "scripts": {
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@aws-amplify/auth": "^5.1.8", "@aws-amplify/auth": "^5.6.5",
"@aws-amplify/core": "^5.0.14", "@aws-amplify/core": "^5.8.5",
"@fortawesome/free-brands-svg-icons": "^6.3.0", "@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"enso-common": "^1.0.0", "enso-common": "^1.0.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
@ -21,6 +23,10 @@
"ts-results": "^3.3.0" "ts-results": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.9.3" "typescript": "~5.2.2"
},
"overrides": {
"@aws-amplify/auth": "../_IGNORED_",
"react-native-url-polyfill": "../_IGNORED_"
} }
} }

View File

@ -94,16 +94,15 @@ interface UserInfo {
* *
* Some Amplify errors (e.g., network connectivity errors) can not be resolved within the * Some Amplify errors (e.g., network connectivity errors) can not be resolved within the
* application. Un-resolvable errors are allowed to flow up to the top-level error handler. Errors * application. Un-resolvable errors are allowed to flow up to the top-level error handler. Errors
* that can be resolved must be caught and handled as early as possible. The {@link KNOWN_ERRORS} * that can be resolved must be caught and handled as early as possible.
* map lists the Amplify errors that we want to catch and convert to typed responses.
* *
* # Handling Amplify Errors * # Handling Amplify Errors
* *
* Use the {@link isAmplifyError} function to check if an `unknown` error is an * Use the {@link isAmplifyError} function to check if an `unknown` error is an
* {@link AmplifyError}. If it is, use the {@link intoAmplifyErrorOrThrow} function to convert it * {@link AmplifyError}. If it is, use the {@link intoAmplifyErrorOrThrow} function to convert it
* from `unknown` to a typed object. Then, use the {@link KNOWN_ERRORS} to see if the error is one * from `unknown` to a typed object. Then, use one of the response error handling functions (e.g.
* that must be handled by the application (i.e., it is an error that is relevant to our business * {@link intoSignUpErrorOrThrow}) to see if the error is one that must be handled by the
* logic). */ * application (i.e., it is an error that is relevant to our business logic). */
interface AmplifyError extends Error { interface AmplifyError extends Error {
/** Error code for disambiguating the error. */ /** Error code for disambiguating the error. */
code: string code: string
@ -396,9 +395,12 @@ const CURRENT_SESSION_NO_CURRENT_USER_ERROR = {
/** Internal IDs of errors that may occur when getting the current session. */ /** Internal IDs of errors that may occur when getting the current session. */
type CurrentSessionErrorKind = (typeof CURRENT_SESSION_NO_CURRENT_USER_ERROR)['kind'] type CurrentSessionErrorKind = (typeof CURRENT_SESSION_NO_CURRENT_USER_ERROR)['kind']
/** Convert an {@link AmplifyError} into a {@link CurrentSessionErrorKind} if it is a known error, /**
* Convert an {@link AmplifyError} into a {@link CurrentSessionErrorKind} if it is a known error,
* else re-throws the error. * else re-throws the error.
* @throws {Error} If the error is not recognized. */ *
* @throws {Error} If the error is not recognized.
*/
function intoCurrentSessionErrorKind(error: unknown): CurrentSessionErrorKind { function intoCurrentSessionErrorKind(error: unknown): CurrentSessionErrorKind {
if (error === CURRENT_SESSION_NO_CURRENT_USER_ERROR.internalMessage) { if (error === CURRENT_SESSION_NO_CURRENT_USER_ERROR.internalMessage) {
return CURRENT_SESSION_NO_CURRENT_USER_ERROR.kind return CURRENT_SESSION_NO_CURRENT_USER_ERROR.kind
@ -467,9 +469,12 @@ export interface SignUpError extends CognitoError {
message: string message: string
} }
/** Convert an {@link AmplifyError} into a {@link SignUpError} if it is a known error, /**
* Convert an {@link AmplifyError} into a {@link SignUpError} if it is a known error,
* else re-throws the error. * else re-throws the error.
* @throws {Error} If the error is not recognized. */ *
* @throws {Error} If the error is not recognized.
*/
function intoSignUpErrorOrThrow(error: AmplifyError): SignUpError { function intoSignUpErrorOrThrow(error: AmplifyError): SignUpError {
if (error.code === SIGN_UP_USERNAME_EXISTS_ERROR.internalCode) { if (error.code === SIGN_UP_USERNAME_EXISTS_ERROR.internalCode) {
return { return {

View File

@ -42,12 +42,12 @@ function isAuthEvent(value: string): value is AuthEvent {
/** Callback called in response to authentication state changes. /** Callback called in response to authentication state changes.
* *
* @see {@link Api["listen"]}. */ * @see {@link amplify.Hub.listen}. */
export type ListenerCallback = (event: AuthEvent, data?: unknown) => void export type ListenerCallback = (event: AuthEvent, data?: unknown) => void
/** Unsubscribe the {@link ListenerCallback} from authentication state changes. /** Unsubscribe the {@link ListenerCallback} from authentication state changes.
* *
* @see {@link Api["listen"]}. */ * @see {@link amplify.Hub.listen}. */
type UnsubscribeFunction = () => void type UnsubscribeFunction = () => void
/** Used to subscribe to {@link AuthEvent}s. /** Used to subscribe to {@link AuthEvent}s.

View File

@ -108,7 +108,7 @@ export type UserSession = FullUserSession | OfflineUserSession | PartialUserSess
* signing out, etc. All interactions with the authentication API should be done through this * signing out, etc. All interactions with the authentication API should be done through this
* interface. * interface.
* *
* See {@link Cognito} for details on each of the authentication functions. */ * See {@link cognito.Cognito} for details on each of the authentication functions. */
interface AuthContextType { interface AuthContextType {
goOffline: (shouldShowToast?: boolean) => Promise<boolean> goOffline: (shouldShowToast?: boolean) => Promise<boolean>
signUp: (email: string, password: string, organizationId: string | null) => Promise<boolean> signUp: (email: string, password: string, organizationId: string | null) => Promise<boolean>

View File

@ -71,36 +71,6 @@ export const Subject = newtype.newtypeConstructor<Subject>()
/* eslint-enable @typescript-eslint/no-redeclare */ /* eslint-enable @typescript-eslint/no-redeclare */
// ========================
// === PermissionAction ===
// ========================
/** Backend representation of user permission types. */
export enum PermissionAction {
own = 'Own',
admin = 'Admin',
edit = 'Edit',
read = 'Read',
readAndDocs = 'Read_docs',
readAndExec = 'Read_exec',
view = 'View',
viewAndDocs = 'View_docs',
viewAndExec = 'View_exec',
}
/** Whether each {@link PermissionAction} can execute a project. */
export const PERMISSION_ACTION_CAN_EXECUTE: Record<PermissionAction, boolean> = {
[PermissionAction.own]: true,
[PermissionAction.admin]: true,
[PermissionAction.edit]: true,
[PermissionAction.read]: false,
[PermissionAction.readAndDocs]: false,
[PermissionAction.readAndExec]: true,
[PermissionAction.view]: false,
[PermissionAction.viewAndDocs]: false,
[PermissionAction.viewAndExec]: true,
}
// ============= // =============
// === Types === // === Types ===
// ============= // =============
@ -356,7 +326,7 @@ export interface SimpleUser {
/** User permission for a specific user. */ /** User permission for a specific user. */
export interface UserPermission { export interface UserPermission {
user: User user: User
permission: PermissionAction permission: permissions.PermissionAction
} }
/** The type returned from the "update directory" endpoint. */ /** The type returned from the "update directory" endpoint. */
@ -571,7 +541,7 @@ export interface InviteUserRequestBody {
export interface CreatePermissionRequestBody { export interface CreatePermissionRequestBody {
userSubjects: Subject[] userSubjects: Subject[]
resourceId: AssetId resourceId: AssetId
action: PermissionAction | null action: permissions.PermissionAction | null
} }
/** HTTP request body for the "create directory" endpoint. */ /** HTTP request body for the "create directory" endpoint. */

View File

@ -17,6 +17,7 @@ import * as authProvider from '../authentication/providers/auth'
import * as backend from './backend' import * as backend from './backend'
import * as dateTime from './dateTime' import * as dateTime from './dateTime'
import * as modalProvider from '../providers/modal' import * as modalProvider from '../providers/modal'
import * as permissions from './permissions'
import * as sorting from './sorting' import * as sorting from './sorting'
import * as tableColumn from './components/tableColumn' import * as tableColumn from './components/tableColumn'
import * as uniqueString from '../uniqueString' import * as uniqueString from '../uniqueString'
@ -109,7 +110,7 @@ export const COLUMN_CSS_CLASS: Record<Column, string> = {
[Column.docs]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`, [Column.docs]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
} as const } as const
/** {@link table.ColumnProps} for an unknown variant of {@link backend.Asset}. */ /** {@link tableColumn.TableColumnProps} for an unknown variant of {@link backend.Asset}. */
export type AssetColumnProps = tableColumn.TableColumnProps< export type AssetColumnProps = tableColumn.TableColumnProps<
assetTreeNode.AssetTreeNode, assetTreeNode.AssetTreeNode,
assetsTable.AssetsTableState, assetsTable.AssetsTableState,
@ -165,8 +166,8 @@ function SharedWithColumn(props: AssetColumnProps) {
permission => permission.user.user_email === session.organization?.email permission => permission.user.user_email === session.organization?.email
) )
const managesThisAsset = const managesThisAsset =
self?.permission === backend.PermissionAction.own || self?.permission === permissions.PermissionAction.own ||
self?.permission === backend.PermissionAction.admin self?.permission === permissions.PermissionAction.admin
const setAsset = React.useCallback( const setAsset = React.useCallback(
(valueOrUpdater: React.SetStateAction<backend.AnyAsset>) => { (valueOrUpdater: React.SetStateAction<backend.AnyAsset>) => {
if (typeof valueOrUpdater === 'function') { if (typeof valueOrUpdater === 'function') {

View File

@ -7,6 +7,7 @@ import * as assetTreeNode from '../assetTreeNode'
import * as backendModule from '../backend' import * as backendModule from '../backend'
import * as hooks from '../../hooks' import * as hooks from '../../hooks'
import * as http from '../../http' import * as http from '../../http'
import * as permissions from '../permissions'
import * as remoteBackendModule from '../remoteBackend' import * as remoteBackendModule from '../remoteBackend'
import * as shortcuts from '../shortcuts' import * as shortcuts from '../shortcuts'
@ -67,14 +68,14 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
) )
const managesThisAsset = const managesThisAsset =
backend.type === backendModule.BackendType.local || backend.type === backendModule.BackendType.local ||
self?.permission === backendModule.PermissionAction.own || self?.permission === permissions.PermissionAction.own ||
self?.permission === backendModule.PermissionAction.admin self?.permission === permissions.PermissionAction.admin
const isRunningProject = const isRunningProject =
asset.type === backendModule.AssetType.project && asset.type === backendModule.AssetType.project &&
backendModule.DOES_PROJECT_STATE_INDICATE_VM_EXISTS[asset.projectState.type] backendModule.DOES_PROJECT_STATE_INDICATE_VM_EXISTS[asset.projectState.type]
const canExecute = const canExecute =
backend.type === backendModule.BackendType.local || backend.type === backendModule.BackendType.local ||
(self?.permission != null && backendModule.PERMISSION_ACTION_CAN_EXECUTE[self.permission]) (self?.permission != null && permissions.PERMISSION_ACTION_CAN_EXECUTE[self.permission])
const isOtherUserUsingProject = const isOtherUserUsingProject =
backend.type !== backendModule.BackendType.local && backend.type !== backendModule.BackendType.local &&
backendModule.assetIsProject(asset) && backendModule.assetIsProject(asset) &&

View File

@ -5,8 +5,6 @@ import * as backendModule from '../backend'
import * as hooks from '../../hooks' import * as hooks from '../../hooks'
import * as load from '../load' import * as load from '../load'
import GLOBAL_CONFIG from '../../../../../../../../gui/config.yaml' assert { type: 'yaml' }
// ================= // =================
// === Constants === // === Constants ===
// ================= // =================
@ -114,15 +112,10 @@ export default function Editor(props: EditorProps) {
} }
} }
const runNewProject = async () => { const runNewProject = async () => {
const engineConfig = const engineConfig = {
backendType === backendModule.BackendType.remote rpcUrl: jsonAddress,
? { dataUrl: binaryAddress,
rpcUrl: jsonAddress, }
dataUrl: binaryAddress,
}
: {
projectManagerUrl: GLOBAL_CONFIG.projectManagerEndpoint,
}
const originalUrl = window.location.href const originalUrl = window.location.href
if (backendType === backendModule.BackendType.remote) { if (backendType === backendModule.BackendType.remote) {
// The URL query contains commandline options when running in the desktop, // The URL query contains commandline options when running in the desktop,

View File

@ -7,6 +7,7 @@ import * as backendModule from '../backend'
import * as backendProvider from '../../providers/backend' import * as backendProvider from '../../providers/backend'
import * as hooks from '../../hooks' import * as hooks from '../../hooks'
import * as modalProvider from '../../providers/modal' import * as modalProvider from '../../providers/modal'
import * as permissionsModule from '../permissions'
import Autocomplete from './autocomplete' import Autocomplete from './autocomplete'
import Modal from './modal' import Modal from './modal'
@ -27,7 +28,7 @@ const TYPE_SELECTOR_Y_OFFSET_PX = 32
/** Props for a {@link ManagePermissionsModal}. */ /** Props for a {@link ManagePermissionsModal}. */
export interface ManagePermissionsModalProps< export interface ManagePermissionsModalProps<
Asset extends backendModule.AnyAsset = backendModule.AnyAsset Asset extends backendModule.AnyAsset = backendModule.AnyAsset,
> { > {
item: Asset item: Asset
setItem: React.Dispatch<React.SetStateAction<Asset>> setItem: React.Dispatch<React.SetStateAction<Asset>>
@ -43,7 +44,7 @@ export interface ManagePermissionsModalProps<
* @throws {Error} when the current backend is the local backend, or when the user is offline. * @throws {Error} when the current backend is the local backend, or when the user is offline.
* This should never happen, as this modal should not be accessible in either case. */ * This should never happen, as this modal should not be accessible in either case. */
export default function ManagePermissionsModal< export default function ManagePermissionsModal<
Asset extends backendModule.AnyAsset = backendModule.AnyAsset Asset extends backendModule.AnyAsset = backendModule.AnyAsset,
>(props: ManagePermissionsModalProps<Asset>) { >(props: ManagePermissionsModalProps<Asset>) {
const { item, setItem, self, doRemoveSelf, eventTarget } = props const { item, setItem, self, doRemoveSelf, eventTarget } = props
const { organization } = auth.useNonPartialUserSession() const { organization } = auth.useNonPartialUserSession()
@ -53,15 +54,15 @@ export default function ManagePermissionsModal<
const [permissions, setPermissions] = React.useState(item.permissions ?? []) const [permissions, setPermissions] = React.useState(item.permissions ?? [])
const [users, setUsers] = React.useState<backendModule.SimpleUser[]>([]) const [users, setUsers] = React.useState<backendModule.SimpleUser[]>([])
const [email, setEmail] = React.useState<string | null>(null) const [email, setEmail] = React.useState<string | null>(null)
const [action, setAction] = React.useState(backendModule.PermissionAction.view) const [action, setAction] = React.useState(permissionsModule.PermissionAction.view)
const emailValidityRef = React.useRef<HTMLInputElement>(null) const emailValidityRef = React.useRef<HTMLInputElement>(null)
const position = React.useMemo(() => eventTarget?.getBoundingClientRect(), [eventTarget]) const position = React.useMemo(() => eventTarget?.getBoundingClientRect(), [eventTarget])
const editablePermissions = React.useMemo( const editablePermissions = React.useMemo(
() => () =>
self.permission === backendModule.PermissionAction.own self.permission === permissionsModule.PermissionAction.own
? permissions ? permissions
: permissions.filter( : permissions.filter(
permission => permission.permission !== backendModule.PermissionAction.own permission => permission.permission !== permissionsModule.PermissionAction.own
), ),
[permissions, self.permission] [permissions, self.permission]
) )
@ -78,10 +79,10 @@ export default function ManagePermissionsModal<
) )
const isOnlyOwner = React.useMemo( const isOnlyOwner = React.useMemo(
() => () =>
self.permission === backendModule.PermissionAction.own && self.permission === permissionsModule.PermissionAction.own &&
permissions.every( permissions.every(
permission => permission =>
permission.permission !== backendModule.PermissionAction.own || permission.permission !== permissionsModule.PermissionAction.own ||
permission.user.user_email === organization?.email permission.user.user_email === organization?.email
), ),
[organization?.email, permissions, self.permission] [organization?.email, permissions, self.permission]
@ -278,7 +279,7 @@ export default function ManagePermissionsModal<
disabled={willInviteNewUser} disabled={willInviteNewUser}
selfPermission={self.permission} selfPermission={self.permission}
typeSelectorYOffsetPx={TYPE_SELECTOR_Y_OFFSET_PX} typeSelectorYOffsetPx={TYPE_SELECTOR_Y_OFFSET_PX}
action={backendModule.PermissionAction.view} action={permissionsModule.PermissionAction.view}
assetType={item.type} assetType={item.type}
onChange={setAction} onChange={setAction}
/> />

View File

@ -1,7 +1,6 @@
/** @file Colored border around icons and text indicating permissions. */ /** @file Colored border around icons and text indicating permissions. */
import * as React from 'react' import * as React from 'react'
import * as backend from '../backend'
import * as permissionsModule from '../permissions' import * as permissionsModule from '../permissions'
// ================= // =================
@ -10,7 +9,7 @@ import * as permissionsModule from '../permissions'
/** Props for a {@link PermissionDisplay}. */ /** Props for a {@link PermissionDisplay}. */
export interface PermissionDisplayProps extends React.PropsWithChildren { export interface PermissionDisplayProps extends React.PropsWithChildren {
action: backend.PermissionAction action: permissionsModule.PermissionAction
className?: string className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> onClick?: React.MouseEventHandler<HTMLDivElement>
onMouseEnter?: React.MouseEventHandler<HTMLDivElement> onMouseEnter?: React.MouseEventHandler<HTMLDivElement>

View File

@ -2,6 +2,7 @@
import * as React from 'react' import * as React from 'react'
import * as backend from '../backend' import * as backend from '../backend'
import * as permissions from '../permissions'
import * as permissionsModule from '../permissions' import * as permissionsModule from '../permissions'
import Modal from './modal' import Modal from './modal'
@ -34,12 +35,12 @@ export interface PermissionSelectorProps {
/** Overrides the vertical offset of the {@link PermissionTypeSelector}. */ /** Overrides the vertical offset of the {@link PermissionTypeSelector}. */
typeSelectorYOffsetPx?: number typeSelectorYOffsetPx?: number
error?: string | null error?: string | null
selfPermission: backend.PermissionAction selfPermission: permissions.PermissionAction
/** If this prop changes, the internal state will be updated too. */ /** If this prop changes, the internal state will be updated too. */
action: backend.PermissionAction action: permissions.PermissionAction
assetType: backend.AssetType assetType: backend.AssetType
className?: string className?: string
onChange: (action: backend.PermissionAction) => void onChange: (action: permissions.PermissionAction) => void
doDelete?: () => void doDelete?: () => void
} }
@ -61,7 +62,7 @@ export default function PermissionSelector(props: PermissionSelectorProps) {
const [TheChild, setTheChild] = React.useState<(() => JSX.Element) | null>() const [TheChild, setTheChild] = React.useState<(() => JSX.Element) | null>()
const permission = permissionsModule.FROM_PERMISSION_ACTION[action] const permission = permissionsModule.FROM_PERMISSION_ACTION[action]
const setAction = (newAction: backend.PermissionAction) => { const setAction = (newAction: permissions.PermissionAction) => {
setActionRaw(newAction) setActionRaw(newAction)
onChange(newAction) onChange(newAction)
} }

View File

@ -70,7 +70,7 @@ const PERMISSION_TYPE_DATA: PermissionTypeData[] = [
/** Props for a {@link PermissionTypeSelector}. */ /** Props for a {@link PermissionTypeSelector}. */
export interface PermissionTypeSelectorProps { export interface PermissionTypeSelectorProps {
showDelete?: boolean showDelete?: boolean
selfPermission: backend.PermissionAction selfPermission: permissions.PermissionAction
type: permissions.Permission type: permissions.Permission
assetType: backend.AssetType assetType: backend.AssetType
style?: React.CSSProperties style?: React.CSSProperties
@ -93,7 +93,7 @@ export default function PermissionTypeSelector(props: PermissionTypeSelectorProp
{PERMISSION_TYPE_DATA.filter( {PERMISSION_TYPE_DATA.filter(
data => data =>
(showDelete ? true : data.type !== permissions.Permission.delete) && (showDelete ? true : data.type !== permissions.Permission.delete) &&
(selfPermission === backend.PermissionAction.own (selfPermission === permissions.PermissionAction.own
? true ? true
: data.type !== permissions.Permission.owner) : data.type !== permissions.Permission.owner)
).map(data => ( ).map(data => (

View File

@ -13,6 +13,7 @@ import * as errorModule from '../../error'
import * as eventModule from '../event' import * as eventModule from '../event'
import * as hooks from '../../hooks' import * as hooks from '../../hooks'
import * as indent from '../indent' import * as indent from '../indent'
import * as permissions from '../permissions'
import * as presence from '../presence' import * as presence from '../presence'
import * as shortcutsModule from '../shortcuts' import * as shortcutsModule from '../shortcuts'
import * as shortcutsProvider from '../../providers/shortcuts' import * as shortcutsProvider from '../../providers/shortcuts'
@ -66,7 +67,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
const canExecute = const canExecute =
backend.type === backendModule.BackendType.local || backend.type === backendModule.BackendType.local ||
(ownPermission != null && (ownPermission != null &&
backendModule.PERMISSION_ACTION_CAN_EXECUTE[ownPermission.permission]) permissions.PERMISSION_ACTION_CAN_EXECUTE[ownPermission.permission])
const isOtherUserUsingProject = const isOtherUserUsingProject =
backend.type !== backendModule.BackendType.local && backend.type !== backendModule.BackendType.local &&
asset.projectState.opened_by != null && asset.projectState.opened_by != null &&

View File

@ -77,7 +77,7 @@ export type TableProps<
T, T,
State = never, State = never,
RowState = never, RowState = never,
Key extends string = string Key extends string = string,
> = InternalTableProps<T, State, RowState, Key> & > = InternalTableProps<T, State, RowState, Key> &
([RowState] extends [never] ? unknown : InitialRowStateProp<RowState>) & ([RowState] extends [never] ? unknown : InitialRowStateProp<RowState>) &
([State] extends [never] ? unknown : StateProp<State>) & ([State] extends [never] ? unknown : StateProp<State>) &

View File

@ -4,7 +4,7 @@
// === Types === // === Types ===
// ============= // =============
/** Props for a {@link Column}. */ /** Props for a {@link TableColumn}. */
export interface TableColumnProps<T, State = never, RowState = never, Key extends string = string> { export interface TableColumnProps<T, State = never, RowState = never, Key extends string = string> {
keyProp: Key keyProp: Key
item: T item: T
@ -16,7 +16,7 @@ export interface TableColumnProps<T, State = never, RowState = never, Key extend
setRowState: React.Dispatch<React.SetStateAction<RowState>> setRowState: React.Dispatch<React.SetStateAction<RowState>>
} }
/** Props for a {@link Column}. */ /** Props for a {@link TableColumn}. */
export interface TableColumnHeadingProps<State = never> { export interface TableColumnHeadingProps<State = never> {
state: State state: State
} }

Some files were not shown because too many files have changed in this diff Show More