mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
[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:
parent
3ba2f6f391
commit
42a7cb2d23
2
.github/workflows/gui.yml
vendored
2
.github/workflows/gui.yml
vendored
@ -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
3
.gitignore
vendored
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
10
.vscode/settings.json
vendored
@ -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
34
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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" }
|
||||||
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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: '^_',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
5
app/gui2/env.d.ts
vendored
@ -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
41
app/gui2/eslint.config.js
Normal 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
|
@ -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
5
app/gui2/node.env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module 'tailwindcss/nesting' {
|
||||||
|
import { PluginCreator } from 'postcss'
|
||||||
|
declare const plugin: PluginCreator<unknown>
|
||||||
|
export default plugin
|
||||||
|
}
|
6505
app/gui2/package-lock.json
generated
6505
app/gui2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
904
app/gui2/rust-ffi/Cargo.lock
generated
904
app/gui2/rust-ffi/Cargo.lock
generated
@ -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",
|
|
||||||
]
|
|
@ -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']
|
|
||||||
|
@ -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
61
app/gui2/shared/event.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
94
app/gui2/shared/languageServer.ts
Normal file
94
app/gui2/shared/languageServer.ts
Normal 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
|
||||||
|
}
|
301
app/gui2/shared/languageServerTypes.ts
Normal file
301
app/gui2/shared/languageServerTypes.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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
355
app/gui2/shared/yjsModel.ts
Normal 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),
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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%;
|
||||||
|
@ -9,4 +9,5 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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'],
|
||||||
|
@ -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 },
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
@ -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<{
|
||||||
|
@ -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 {
|
||||||
|
@ -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">@ </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,
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
msg: string
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="greetings">
|
|
||||||
<h1 class="green">{{ msg }}</h1>
|
|
||||||
<h3>
|
|
||||||
You’ve 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>
|
|
@ -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>
|
||||||
|
@ -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<{
|
||||||
|
@ -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)"
|
||||||
/>
|
/>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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] }>()
|
||||||
|
@ -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()
|
||||||
|
26
app/gui2/src/providers/guiConfig.ts
Normal file
26
app/gui2/src/providers/guiConfig.ts
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"]
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)),
|
||||||
},
|
},
|
||||||
|
83
app/gui2/ydoc-server/index.ts
Normal file
83
app/gui2/ydoc-server/index.ts
Normal 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
|
||||||
|
}
|
266
app/gui2/ydoc-server/languageServerSession.ts
Normal file
266
app/gui2/ydoc-server/languageServerSession.ts
Normal 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(() => {})
|
||||||
|
}
|
||||||
|
}
|
235
app/gui2/ydoc-server/ydoc.ts
Normal file
235
app/gui2/ydoc-server/ydoc.ts
Normal 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', [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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 ===
|
||||||
|
@ -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 ===
|
||||||
// ================
|
// ================
|
||||||
|
@ -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) {
|
@ -4,5 +4,12 @@
|
|||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["../types", "."]
|
"include": [
|
||||||
|
".",
|
||||||
|
"../content",
|
||||||
|
"../dashboard",
|
||||||
|
"../types",
|
||||||
|
"../../utils.ts",
|
||||||
|
"../../../../build.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"include": ["../types", "."]
|
"include": ["../types", "../../../../build.json", ".", "src/config.json"]
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
||||||
|
@ -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",
|
||||||
|
@ -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" />
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"include": ["../types", "."]
|
"include": [
|
||||||
|
"../types",
|
||||||
|
"../../utils.ts",
|
||||||
|
"../../../../build.json",
|
||||||
|
"../dashboard",
|
||||||
|
"."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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",
|
||||||
|
@ -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_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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.
|
||||||
|
@ -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>
|
||||||
|
@ -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. */
|
||||||
|
@ -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') {
|
||||||
|
@ -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) &&
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 => (
|
||||||
|
@ -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 &&
|
||||||
|
@ -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>) &
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user