mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 03:32:23 +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
|
||||
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
|
||||
name: Installing wasm-pack
|
||||
uses: jetli/wasm-pack-action@v0.3.0
|
||||
uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.10.2
|
||||
- name: Expose Artifact API and context information.
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -70,6 +70,7 @@ node_modules/
|
||||
.enso-sources*
|
||||
.metals
|
||||
tools/performance/engine-benchmarks/generated_site
|
||||
*.tsbuildinfo
|
||||
|
||||
############################
|
||||
## Rendered Documentation ##
|
||||
@ -108,7 +109,7 @@ bench-report*.xml
|
||||
.bloop/
|
||||
.bsp/
|
||||
project/metals.sbt
|
||||
/app/ide-desktop/build.json
|
||||
/build.json
|
||||
/app/ide-desktop/lib/client/electron-builder-config.json
|
||||
|
||||
|
||||
|
@ -26,11 +26,13 @@ test/**/data
|
||||
**/msdfgen_wasm.js
|
||||
|
||||
# Generated files
|
||||
app/ide-desktop/build.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/gui2/rust-ffi/pkg
|
||||
Cargo.lock
|
||||
build.json
|
||||
app/gui2/playwright-report/
|
||||
|
||||
# Engine Builds can leave these nested working copies.
|
||||
# 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": [
|
||||
{ "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": {
|
||||
"**/target": true
|
||||
}
|
||||
|
34
Cargo.lock
generated
34
Cargo.lock
generated
@ -5995,6 +5995,16 @@ dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"enso-parser",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.21"
|
||||
@ -7425,9 +7435,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.84"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
|
||||
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"wasm-bindgen-macro",
|
||||
@ -7435,16 +7445,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.84"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
|
||||
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.107",
|
||||
"syn 2.0.15",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -7462,9 +7472,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.84"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
|
||||
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@ -7472,22 +7482,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.84"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
|
||||
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.107",
|
||||
"syn 2.0.15",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.84"
|
||||
version = "0.2.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
|
@ -10,6 +10,7 @@ members = [
|
||||
"app/gui",
|
||||
"app/gui/language/parser",
|
||||
"app/gui/enso-profiler-enso-data",
|
||||
"app/gui2/rust-ffi",
|
||||
"build/cli",
|
||||
"build/macros/proc-macro",
|
||||
"build/ci-gen",
|
||||
@ -93,7 +94,7 @@ serde-wasm-bindgen = { version = "0.4.5" }
|
||||
tokio = { version = "1.23.0", features = ["full", "tracing"] }
|
||||
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||
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" }
|
||||
anyhow = { version = "1.0.66" }
|
||||
failure = { version = "0.1.8" }
|
||||
|
@ -60,52 +60,43 @@ impl BackendService {
|
||||
/// Read backend configuration from the web arguments. See also [`web::Arguments`]
|
||||
/// documentation.
|
||||
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 data_url_option = &args.groups.engine.options.data_url;
|
||||
let rpc_url = rpc_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() {
|
||||
Err(MutuallyExclusiveOptions.into())
|
||||
} else {
|
||||
let endpoint = endpoint.to_owned();
|
||||
Ok(Self::ProjectManager { endpoint })
|
||||
}
|
||||
} else {
|
||||
match (rpc_url, data_url) {
|
||||
("", "") => Ok(default()),
|
||||
("", _) => Err(MissingOption(rpc_url_option.__name__.to_owned()).into()),
|
||||
(_, "") => Err(MissingOption(data_url_option.__name__.to_owned()).into()),
|
||||
(json_endpoint, binary_endpoint) => {
|
||||
let json_endpoint = json_endpoint.to_owned();
|
||||
let binary_endpoint = binary_endpoint.to_owned();
|
||||
let def_namespace = || constants::DEFAULT_PROJECT_NAMESPACE.to_owned();
|
||||
let namespace = args.groups.engine.options.namespace.value.clone();
|
||||
let namespace = if namespace.is_empty() { def_namespace() } else { namespace };
|
||||
let project_name_option = &args.groups.startup.options.project;
|
||||
let project_name = project_name_option.value.as_str();
|
||||
let no_project_name = || MissingOption(project_name_option.__name__.to_owned());
|
||||
let project_name = if project_name.is_empty() {
|
||||
Err(no_project_name())
|
||||
} else {
|
||||
Ok(project_name.to_owned())
|
||||
}?;
|
||||
let displayed_name_option = &args.groups.startup.options.displayed_project_name;
|
||||
let displayed_name = displayed_name_option.value.as_str();
|
||||
let displayed_name = if displayed_name.is_empty() {
|
||||
project_name.clone()
|
||||
} else {
|
||||
displayed_name.to_owned()
|
||||
};
|
||||
Ok(Self::LanguageServer {
|
||||
json_endpoint,
|
||||
binary_endpoint,
|
||||
namespace,
|
||||
project_name,
|
||||
displayed_name,
|
||||
})
|
||||
}
|
||||
|
||||
match (rpc_url, data_url) {
|
||||
("", "") => Ok(default()),
|
||||
("", _) => Err(MissingOption(rpc_url_option.__name__.to_owned()).into()),
|
||||
(_, "") => Err(MissingOption(data_url_option.__name__.to_owned()).into()),
|
||||
(json_endpoint, binary_endpoint) => {
|
||||
let json_endpoint = json_endpoint.to_owned();
|
||||
let binary_endpoint = binary_endpoint.to_owned();
|
||||
let def_namespace = || constants::DEFAULT_PROJECT_NAMESPACE.to_owned();
|
||||
let namespace = args.groups.engine.options.namespace.value.clone();
|
||||
let namespace = if namespace.is_empty() { def_namespace() } else { namespace };
|
||||
let project_name_option = &args.groups.startup.options.project;
|
||||
let project_name = project_name_option.value.as_str();
|
||||
let no_project_name = || MissingOption(project_name_option.__name__.to_owned());
|
||||
let project_name = if project_name.is_empty() {
|
||||
Err(no_project_name())
|
||||
} else {
|
||||
Ok(project_name.to_owned())
|
||||
}?;
|
||||
let displayed_name_option = &args.groups.startup.options.displayed_project_name;
|
||||
let displayed_name = displayed_name_option.value.as_str();
|
||||
let displayed_name = if displayed_name.is_empty() {
|
||||
project_name.clone()
|
||||
} else {
|
||||
displayed_name.to_owned()
|
||||
};
|
||||
Ok(Self::LanguageServer {
|
||||
json_endpoint,
|
||||
binary_endpoint,
|
||||
namespace,
|
||||
project_name,
|
||||
displayed_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,8 @@
|
||||
.enso-internal-templates-view .enso-internal-card {
|
||||
border-radius: 20px;
|
||||
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 8.04107px 11.3915px rgba(0, 0, 0, 0.0197608),
|
||||
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",
|
||||
"plugins": ["prettier-plugin-organize-imports"],
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"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:
|
||||
// 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" />
|
||||
|
||||
module 'y-websocket' {
|
||||
// hack for bad module resolution
|
||||
export * from 'node_modules/y-websocket/dist/src/y-websocket'
|
||||
}
|
||||
declare const PROJECT_MANAGER_URL: string
|
||||
|
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">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -8,6 +8,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="enso-dashboard" class="enso-dashboard"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</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.0.0",
|
||||
"version": "0.1.0",
|
||||
"name": "enso-gui2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"author": {
|
||||
"name": "Enso Team",
|
||||
"email": "contact@enso.org"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"build": "run-p typecheck build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test": "vitest run",
|
||||
"build-only": "vite build",
|
||||
"type-check": "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",
|
||||
"format": "prettier --write src/",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write src/ && eslint . --fix",
|
||||
"build-rust-ffi": "cd rust-ffi && wasm-pack build --release --target web",
|
||||
"preinstall": "npm run build-rust-ffi"
|
||||
},
|
||||
"dependencies": {
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"lib0": "^0.2.83",
|
||||
"pinia": "^2.1.6",
|
||||
"postcss-nesting": "^12.0.1",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"postcss-inline-svg": "^6.0.0",
|
||||
"sha3": "^2.1.4",
|
||||
"vue": "^3.3.4",
|
||||
"ws": "^8.13.0",
|
||||
"y-protocols": "^1.0.5",
|
||||
@ -29,6 +38,8 @@
|
||||
"yjs": "^13.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^2.1.2",
|
||||
"@eslint/js": "^8.49.0",
|
||||
"@playwright/test": "^1.37.0",
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@tsconfig/node18": "^18.2.0",
|
||||
@ -36,19 +47,26 @@
|
||||
"@types/node": "^18.17.5",
|
||||
"@types/shuffle-seed": "^1.1.0",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@vitejs/plugin-react": "^4.0.4",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@volar/vue-typescript": "^1.6.5",
|
||||
"@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/tsconfig": "^0.4.0",
|
||||
"eslint": "^8.46.0",
|
||||
"esbuild": "^0.19.3",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.16.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-nesting": "^12.0.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"shuffle-seed": "^1.1.6",
|
||||
"typescript": "~5.1.6",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-inspect": "^0.7.38",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vitest": "^0.34.2",
|
||||
"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"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = { version = "0.2.84", features = [] }
|
||||
enso-parser = { path = "../../../lib/rust/parser" }
|
||||
serde_json = "1.0"
|
||||
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
debug = false
|
||||
strip = true
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ['-Os']
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
console_error_panic_hook = { workspace = true }
|
||||
|
@ -19,3 +19,8 @@ pub fn parse_to_json(code: &str) -> String {
|
||||
let ast = PARSER.with(|parser| parser.run(code));
|
||||
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">
|
||||
import { onMounted } from 'vue'
|
||||
import { provideGuiConfig, type GuiConfig } from '@/providers/guiConfig'
|
||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
import ProjectView from '@/views/ProjectView.vue'
|
||||
import { onMounted, toRef } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
config: GuiConfig
|
||||
}>()
|
||||
|
||||
provideGuiConfig(toRef(props, 'config'))
|
||||
|
||||
onMounted(() => useSuggestionDbStore().initializeDb())
|
||||
</script>
|
||||
|
@ -71,9 +71,22 @@ body {
|
||||
color: var(--color-text);
|
||||
/* TEMPORARY. Will be replaced with actual background when it is integrated with the dashboard. */
|
||||
background: #e4d4be;
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
transition:
|
||||
color 0.5s,
|
||||
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-weight: 500;
|
||||
line-height: 174.5%;
|
||||
|
@ -9,4 +9,5 @@ body {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
font-weight: normal;
|
||||
cursor: default;
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
<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 { type Component, makeComponentList } from '@/components/ComponentBrowser/component'
|
||||
import type { useNavigator } from '@/util/navigator'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
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 TOP_BAR_HEIGHT = 32
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
import {
|
||||
compareSuggestions,
|
||||
labelOfEntry,
|
||||
type MatchedSuggestion,
|
||||
} from '@/components/ComponentBrowser/component'
|
||||
import {
|
||||
makeCon,
|
||||
makeMethod,
|
||||
@ -7,13 +12,8 @@ import {
|
||||
makeModuleMethod,
|
||||
makeStaticMethod,
|
||||
} from '@/stores/suggestionDatabase/entry'
|
||||
import {
|
||||
compareSuggestions,
|
||||
labelOfEntry,
|
||||
type MatchedSuggestion,
|
||||
} from '@/components/ComponentBrowser/component'
|
||||
import { Filtering } from '../filtering'
|
||||
import shuffleSeed from 'shuffle-seed'
|
||||
import { Filtering } from '../filtering'
|
||||
|
||||
test.each([
|
||||
[makeModuleMethod('Standard.Base.Data.read'), 'Data.read'],
|
||||
|
@ -10,8 +10,8 @@ import {
|
||||
makeStaticMethod,
|
||||
makeType,
|
||||
} from '@/stores/suggestionDatabase/entry'
|
||||
import { Filtering } from '../filtering'
|
||||
import type { QualifiedName } from '@/util/qualifiedName'
|
||||
import { Filtering } from '../filtering'
|
||||
|
||||
test.each([
|
||||
{ ...makeModuleMethod('Standard.Base.Data.read'), groupIndex: 0 },
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { SuggestionDb } from '@/stores/suggestionDatabase'
|
||||
import {
|
||||
SuggestionKind,
|
||||
type SuggestionEntry,
|
||||
type SuggestionId,
|
||||
} 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 { isSome } from '@/util/opt'
|
||||
import { qnIsTopElement, qnLastSegment } from '@/util/qualifiedName'
|
||||
import { Filtering, type MatchResult } from './filtering'
|
||||
|
||||
export interface Component {
|
||||
suggestionId: SuggestionId
|
||||
|
@ -108,8 +108,8 @@ class FilteringWithPattern {
|
||||
if (this.initialsMatchRegex.test(entry.name)) {
|
||||
return { score: MatchTypeScore.NameInitialMatch }
|
||||
}
|
||||
const matchedAliasInitials = entry.aliases.find((alias) =>
|
||||
this.initialsMatchRegex?.test(alias),
|
||||
const matchedAliasInitials = entry.aliases.find(
|
||||
(alias) => this.initialsMatchRegex?.test(alias),
|
||||
)
|
||||
if (matchedAliasInitials) {
|
||||
return { matchedAlias: matchedAliasInitials, score: MatchTypeScore.AliasInitialMatch }
|
||||
|
@ -2,7 +2,7 @@
|
||||
import type { Edge } from '@/stores/graph'
|
||||
import type { Rect } from '@/stores/rect'
|
||||
import { clamp } from '@vueuse/core'
|
||||
import type { ExprId } from 'shared/yjs-model'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -5,12 +5,13 @@ import GraphNode from '@/components/GraphNode.vue'
|
||||
import TopBar from '@/components/TopBar.vue'
|
||||
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import type { Rect } from '@/stores/rect'
|
||||
import { useWindowEvent } from '@/util/events'
|
||||
import { modKey, useWindowEvent } from '@/util/events'
|
||||
import { useNavigator } from '@/util/navigator'
|
||||
import { Vec2 } from '@/util/vec2'
|
||||
import type { ContentRange, ExprId } from 'shared/yjs-model'
|
||||
import { reactive, ref, watchEffect } from 'vue'
|
||||
import type { ContentRange, ExprId } from 'shared/yjsModel'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
const EXECUTION_MODES = ['design', 'live']
|
||||
|
||||
@ -19,13 +20,10 @@ const mode = ref('design')
|
||||
const viewportNode = ref<HTMLElement>()
|
||||
const navigator = useNavigator(viewportNode)
|
||||
const graphStore = useGraphStore()
|
||||
const projectStore = useProjectStore()
|
||||
const componentBrowserVisible = ref(false)
|
||||
const componentBrowserPosition = ref(Vec2.Zero())
|
||||
|
||||
watchEffect(() => {
|
||||
console.log(`execution mode changed to '${mode.value}'.`)
|
||||
})
|
||||
|
||||
const nodeRects = reactive(new Map<ExprId, Rect>())
|
||||
const exprRects = reactive(new Map<ExprId, Rect>())
|
||||
|
||||
@ -53,20 +51,28 @@ function keyboardBusy() {
|
||||
useWindowEvent('keydown', (e) => {
|
||||
if (keyboardBusy()) return
|
||||
const pos = navigator.sceneMousePos
|
||||
if (pos == null) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
if (!componentBrowserVisible.value) {
|
||||
componentBrowserPosition.value = pos
|
||||
componentBrowserVisible.value = true
|
||||
if (modKey(e)) {
|
||||
switch (e.key) {
|
||||
case 'z':
|
||||
projectStore.undoManager.undo()
|
||||
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 {
|
||||
position: relative;
|
||||
contain: layout;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import NodeSpan from '@/components/NodeSpan.vue'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import type { Node } from '@/stores/graph'
|
||||
import { Rect } from '@/stores/rect'
|
||||
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 { ContentRange, ExprId } from 'shared/yjsModel'
|
||||
import { computed, onUpdated, reactive, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
node: Node
|
||||
@ -261,7 +261,7 @@ function handleClick(e: PointerEvent) {
|
||||
:class="{ dragging: dragPointer.dragging }"
|
||||
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
|
||||
ref="editableRootNode"
|
||||
@ -283,6 +283,7 @@ function handleClick(e: PointerEvent) {
|
||||
|
||||
<style scoped>
|
||||
.Node {
|
||||
color: red;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -300,6 +301,7 @@ function handleClick(e: PointerEvent) {
|
||||
|
||||
.binding {
|
||||
margin-right: 10px;
|
||||
color: black;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
@ -314,6 +316,7 @@ function handleClick(e: PointerEvent) {
|
||||
|
||||
.icon {
|
||||
cursor: grab;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -3,7 +3,7 @@ import { spanKindName, type Span } from '@/stores/graph'
|
||||
import { Rect } from '@/stores/rect'
|
||||
import { useResizeObserver } from '@/util/events'
|
||||
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'
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -7,10 +7,10 @@ const emit = defineEmits<{ execute: []; 'update:mode': [mode: string] }>()
|
||||
|
||||
<template>
|
||||
<div class="ProjectTitle">
|
||||
<span class="title" v-text="title"></span>
|
||||
<span class="title" v-text="props.title"></span>
|
||||
<ExecutionModeSelector
|
||||
:modes="modes"
|
||||
:model-value="mode"
|
||||
:modes="props.modes"
|
||||
:model-value="props.mode"
|
||||
@execute="emit('execute')"
|
||||
@update:modelValue="emit('update:mode', $event)"
|
||||
/>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import NavBar from '@/components/NavBar.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 emit = defineEmits<{
|
||||
@ -10,19 +12,28 @@ const emit = defineEmits<{
|
||||
breadcrumbClick: [index: number]
|
||||
'update:mode': [mode: string]
|
||||
}>()
|
||||
|
||||
const config = useGuiConfig()
|
||||
|
||||
const barStyle = computed(() => {
|
||||
const offset = config.value.window?.topBarOffset ?? '0'
|
||||
return {
|
||||
marginLeft: `${offset}px`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="TopBar">
|
||||
<div class="TopBar" :style="barStyle">
|
||||
<ProjectTitle
|
||||
:title="title"
|
||||
:modes="modes"
|
||||
:mode="mode"
|
||||
:title="props.title"
|
||||
:modes="props.modes"
|
||||
:mode="props.mode"
|
||||
@update:mode="emit('update:mode', $event)"
|
||||
@execute="emit('execute')"
|
||||
/>
|
||||
<NavBar
|
||||
:breadcrumbs="breadcrumbs"
|
||||
:breadcrumbs="props.breadcrumbs"
|
||||
@back="emit('back')"
|
||||
@forward="emit('forward')"
|
||||
@breadcrumbClick="emit('breadcrumbClick', $event)"
|
||||
@ -40,3 +51,4 @@ const emit = defineEmits<{
|
||||
left: 9px;
|
||||
}
|
||||
</style>
|
||||
@/providers/guiConfig
|
||||
|
@ -4,8 +4,8 @@ const emit = defineEmits<{ 'update:modelValue': [modelValue: boolean] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="Checkbox" @click="emit('update:modelValue', !modelValue)">
|
||||
<div :class="{ hidden: !modelValue }"></div>
|
||||
<div class="Checkbox" @click="emit('update:modelValue', !props.modelValue)">
|
||||
<div :class="{ hidden: !props.modelValue }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { PointerButtonMask, usePointer } from '@/util/events'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{ modelValue: number; min: number; max: 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 { createApp } from 'vue'
|
||||
import { isMac } from 'lib0/environment'
|
||||
import { decodeQueryParams } from 'lib0/url'
|
||||
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)
|
||||
app.use(createPinia())
|
||||
// Temporary hardcode
|
||||
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 { computed, ref } from 'vue'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { computed, reactive, ref, watch, watchEffect } from 'vue'
|
||||
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 { assert, assertNever } from '@/util/assert'
|
||||
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 {
|
||||
rangeIntersects,
|
||||
type ContentRange,
|
||||
type ExprId,
|
||||
type IdMap,
|
||||
type NodeMetadata,
|
||||
rangeIntersects,
|
||||
} from 'shared/yjs-model'
|
||||
import type { Opt } from '@/util/opt'
|
||||
import { parseEnso } from '@/util/ffi'
|
||||
} from 'shared/yjsModel'
|
||||
import { computed, reactive, ref, watch, watchEffect } from 'vue'
|
||||
import * as Y from 'yjs'
|
||||
import { useProjectStore } from './project'
|
||||
|
||||
export const useGraphStore = defineStore('graph', () => {
|
||||
const proj = useProjectStore()
|
||||
|
||||
proj.setProjectName('test')
|
||||
proj.setObservedFileName('Main.enso')
|
||||
|
||||
const text = computed(() => proj.module?.contents)
|
||||
@ -79,47 +79,50 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
watchEffect(() => {})
|
||||
|
||||
function updateState(affectedRanges?: ContentRange[]) {
|
||||
if (proj.module == null) return
|
||||
const idMap = proj.module.getIdMap()
|
||||
const meta = proj.module.metadata
|
||||
const text = proj.module.contents
|
||||
const textContentLocal = textContent.value
|
||||
const parsed = parseBlock(0, textContentLocal, idMap)
|
||||
const module = proj.module
|
||||
if (module == null) return
|
||||
module.doc.transact(() => {
|
||||
const idMap = module.getIdMap()
|
||||
const meta = module.metadata
|
||||
const text = module.contents
|
||||
const textContentLocal = textContent.value
|
||||
const parsed = parseBlock(0, textContentLocal, idMap)
|
||||
|
||||
_parsed.value = parsed
|
||||
_parsedEnso.value = parseEnso(textContentLocal)
|
||||
_parsed.value = parsed
|
||||
_parsedEnso.value = parseEnso(textContentLocal)
|
||||
|
||||
const accessed = idMap.accessedSoFar()
|
||||
const accessed = idMap.accessedSoFar()
|
||||
|
||||
for (const nodeId of nodes.keys()) {
|
||||
if (!accessed.has(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()
|
||||
for (const nodeId of nodes.keys()) {
|
||||
if (!accessed.has(nodeId)) {
|
||||
nodeDeleted(nodeId)
|
||||
}
|
||||
if (affectedRanges.length === 0) break
|
||||
const nodeAffected = rangeIntersects(exprRange, affectedRanges[0])
|
||||
if (!nodeAffected) continue
|
||||
}
|
||||
idMap.finishAndSynchronize()
|
||||
|
||||
const nodeMeta = meta.get(id)
|
||||
const nodeContent = textContentLocal.substring(exprRange[0], exprRange[1])
|
||||
const node = nodes.get(id)
|
||||
if (node == null) {
|
||||
nodeInserted(stmt, text, nodeContent, nodeMeta)
|
||||
} else {
|
||||
nodeUpdated(node, stmt, text, nodeContent, nodeMeta)
|
||||
for (const stmt of parsed) {
|
||||
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
|
||||
}
|
||||
|
||||
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) => {
|
||||
@ -129,11 +132,13 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
const data = meta.get(id)
|
||||
const node = nodes.get(id as ExprId)
|
||||
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)) {
|
||||
node.position = pos
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(op)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -142,20 +147,20 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
const identUsages = reactive(new Map<string, Set<ExprId>>())
|
||||
|
||||
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 = {
|
||||
content,
|
||||
binding: stmt.binding ?? '',
|
||||
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: [
|
||||
Y.createRelativePositionFromTypeIndex(text, stmt.exprOffset),
|
||||
Y.createRelativePositionFromTypeIndex(text, stmt.exprOffset + stmt.expression.length),
|
||||
],
|
||||
}
|
||||
identDefinitions.set(node.binding, stmt.id)
|
||||
addSpanUsages(stmt.id, node)
|
||||
nodes.set(stmt.id, node)
|
||||
identDefinitions.set(node.binding, nodeId)
|
||||
addSpanUsages(nodeId, node)
|
||||
nodes.set(nodeId, node)
|
||||
}
|
||||
|
||||
function nodeUpdated(
|
||||
@ -165,7 +170,6 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
content: string,
|
||||
meta: Opt<NodeMetadata>,
|
||||
) {
|
||||
console.log('nodeUpdated', stmt.id)
|
||||
clearSpanUsages(stmt.id, node)
|
||||
node.content = content
|
||||
if (node.binding !== stmt.binding) {
|
||||
@ -178,8 +182,8 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
} else {
|
||||
node.rootSpan = stmt.expression
|
||||
}
|
||||
if (meta != null && !node.position.equals(new Vec2(meta.x, meta.y))) {
|
||||
node.position = 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,17 +253,17 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
return edges
|
||||
})
|
||||
|
||||
function createNode(position: Vec2): Opt<ExprId> {
|
||||
function createNode(position: Vec2, expression: string): Opt<ExprId> {
|
||||
const mod = proj.module
|
||||
if (mod == null) return
|
||||
const { contents } = mod
|
||||
|
||||
const meta: NodeMetadata = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
y: -position.y,
|
||||
}
|
||||
const ident = generateUniqueIdent()
|
||||
const content = `${ident} = x`
|
||||
const content = `${ident} = ${expression}`
|
||||
return mod.insertNewNode(contents.length, content, meta)
|
||||
}
|
||||
|
||||
@ -288,7 +292,7 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
function setNodePosition(id: ExprId, position: Vec2) {
|
||||
const node = nodes.get(id)
|
||||
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 {
|
||||
@ -398,10 +402,10 @@ interface Statement {
|
||||
function parseBlock(offset: number, content: string, idMap: IdMap): Statement[] {
|
||||
const stmtRegex = /^( *)(([a-zA-Z0-9_]+) *= *)?(.*)$/gm
|
||||
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
|
||||
const pos = offset + index + ident.length
|
||||
const id = idMap.getOrInsertUniqueId([pos, pos + stmt.length])
|
||||
const pos = offset + index + indent.length
|
||||
const id = idMap.getOrInsertUniqueId([pos, pos + stmt.length - indent.length])
|
||||
const exprOffset = pos + (beforeExpr?.length ?? 0)
|
||||
stmts.push({
|
||||
id,
|
||||
|
@ -1,10 +1,32 @@
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import * as Y from 'yjs'
|
||||
import { useGuiConfig, type GuiConfig } from '@/providers/guiConfig'
|
||||
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 { 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 * 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
|
||||
@ -12,44 +34,77 @@ import { Awareness } from 'y-protocols/awareness'
|
||||
* client, it is submitted to the language server as a document update.
|
||||
*/
|
||||
export const useProjectStore = defineStore('project', () => {
|
||||
// inputs
|
||||
const projectName = ref<string>()
|
||||
const observedFileName = ref<string>()
|
||||
|
||||
const doc = new Y.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) => {
|
||||
// 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.
|
||||
const socketUrl = location.origin.replace(/^http/, 'ws') + '/room'
|
||||
const provider = attachProvider(socketUrl, 'enso-projects', doc, awareness)
|
||||
const socketUrl = new URL(location.origin)
|
||||
socketUrl.protocol = location.protocol.replace(/^http/, 'ws')
|
||||
socketUrl.pathname = '/project'
|
||||
const provider = attachProvider(socketUrl.href, 'index', { ls: lsUrls.rpcUrl }, doc, awareness)
|
||||
onCleanup(() => {
|
||||
provider.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
const model = new DistributedModel(doc)
|
||||
const project = computedAsync(async () => {
|
||||
const name = projectName.value
|
||||
const projectModel = new DistributedProject(doc)
|
||||
const moduleDocGuid = ref<string>()
|
||||
|
||||
function currentDocGuid() {
|
||||
const name = observedFileName.value
|
||||
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 moduleName = observedFileName.value
|
||||
const p = project.value
|
||||
if (moduleName == null || p == null) return
|
||||
return await p.openOrCreateModule(moduleName)
|
||||
const guid = moduleDocGuid.value
|
||||
if (guid == null) return null
|
||||
const moduleName = projectModel.findModuleByDocId(guid)
|
||||
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 {
|
||||
setProjectName(name: string) {
|
||||
projectName.value = name
|
||||
},
|
||||
setObservedFileName(name: string) {
|
||||
observedFileName.value = name
|
||||
},
|
||||
module,
|
||||
undoManager,
|
||||
lsRpcConnection: markRaw(lsRpcConnection),
|
||||
}
|
||||
})
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { assert } from '@/util/assert'
|
||||
import {
|
||||
isIdentifier,
|
||||
isQualifiedName,
|
||||
qnLastSegment,
|
||||
qnParent,
|
||||
qnSplit,
|
||||
type Identifier,
|
||||
type QualifiedName,
|
||||
qnSplit,
|
||||
isQualifiedName,
|
||||
isIdentifier,
|
||||
} from '@/util/qualifiedName'
|
||||
|
||||
export type SuggestionId = number
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { findIndexOpt } from '@/util/array'
|
||||
import { isSome } from '@/util/opt'
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, ref } from 'vue'
|
||||
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 const SuggestionDb = Map<SuggestionId, SuggestionEntry>
|
||||
|
@ -1,13 +1,13 @@
|
||||
export function assertNever(x: never): never {
|
||||
throw new Error('Unexpected object: ' + x)
|
||||
bail('Unexpected object: ' + x)
|
||||
}
|
||||
|
||||
export function assert(condition: boolean): asserts condition {
|
||||
if (!condition) throw new Error('Assertion failed')
|
||||
if (!condition) bail('Assertion failed')
|
||||
}
|
||||
|
||||
export function assertUnreachable(): never {
|
||||
throw new Error('Unreachable code')
|
||||
bail('Unreachable code')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,8 +40,22 @@ interface SubdocsEvent {
|
||||
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 onDrop = () => doc.emit('sync', [false])
|
||||
|
||||
@ -49,8 +63,7 @@ export function attachProvider(url: string, room: string, doc: Y.Doc, awareness:
|
||||
|
||||
function onSubdocs(e: SubdocsEvent) {
|
||||
e.loaded.forEach((subdoc) => {
|
||||
const subdocRoom = `${room}--${subdoc.guid}`
|
||||
attachedSubdocs.set(subdoc, attachProvider(url, subdocRoom, subdoc, awareness))
|
||||
attachedSubdocs.set(subdoc, attachProvider(url, subdoc.guid, params, subdoc, awareness))
|
||||
})
|
||||
e.removed.forEach((subdoc) => {
|
||||
const subdocProvider = attachedSubdocs.get(subdoc)
|
||||
|
@ -4,10 +4,10 @@ import {
|
||||
onUnmounted,
|
||||
proxyRefs,
|
||||
ref,
|
||||
type Ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
watchEffect,
|
||||
type Ref,
|
||||
type WatchSource,
|
||||
} from 'vue'
|
||||
import { Vec2 } from './vec2'
|
||||
@ -108,9 +108,13 @@ export function useDocumentEventConditional<K extends keyof DocumentEventMap>(
|
||||
})
|
||||
}
|
||||
|
||||
// const hasWindow = typeof window !== 'undefined'
|
||||
// const platform = hasWindow ? window.navigator?.platform ?? '' : ''
|
||||
// const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(platform)
|
||||
const hasWindow = typeof window !== 'undefined'
|
||||
const platform = hasWindow ? window.navigator?.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.
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Rect } from '@/stores/rect'
|
||||
import { computed, proxyRefs, ref, type Ref } from 'vue'
|
||||
import { PointerButtonMask, usePointer, useResizeObserver, useWindowEvent } from './events'
|
||||
import { Vec2 } from './vec2'
|
||||
import { Rect } from '@/stores/rect'
|
||||
|
||||
function elemRect(target: Element | undefined): Rect {
|
||||
if (target != null && target instanceof Element) {
|
||||
|
@ -3,11 +3,18 @@
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.json", "src/**/*.vue", "shared/**/*"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"resolvePackageJsonExports": false,
|
||||
"composite": true,
|
||||
"outDir": "../../node_modules/.cache/tsc",
|
||||
"baseUrl": ".",
|
||||
"types": ["vitest/importMeta"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vitest/importMeta"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../ide-desktop/lib/dashboard/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ide-desktop/lib/dashboard/src/authentication/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
|
@ -1,8 +1,16 @@
|
||||
{
|
||||
"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": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
|
@ -5,5 +5,10 @@
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"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 { fileURLToPath } from 'node:url'
|
||||
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 * 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/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), yWebsocketServer(), topLevelAwait()],
|
||||
cacheDir: '../../node_modules/.cache/vite',
|
||||
plugins: [vue(), gatewayServer(), topLevelAwait()],
|
||||
optimizeDeps: {
|
||||
entries: 'index.html',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
shared: fileURLToPath(new URL('./shared', 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: {
|
||||
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 yWebsocketServer(): Plugin {
|
||||
function gatewayServer(): Plugin {
|
||||
return {
|
||||
name: 'y-websocket-server',
|
||||
name: 'gateway-server',
|
||||
configureServer(server) {
|
||||
if (server.httpServer == null) return
|
||||
const { setupWSConnection } = require('./node_modules/y-websocket/bin/utils')
|
||||
const wss = new WebSocketServer({ noServer: true })
|
||||
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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createGatewayServer(server.httpServer)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(
|
||||
@ -7,7 +7,7 @@ export default mergeConfig(
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
includeSource: ['./src/**/*.{ts,vue}'],
|
||||
includeSource: ['./{src,shared}/**/*.{ts,vue}'],
|
||||
exclude: [...configDefaults.exclude, 'e2e/*'],
|
||||
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 fs from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
|
||||
import * as electronBuilder from 'electron-builder'
|
||||
import * as electronNotarize from 'electron-notarize'
|
||||
@ -19,9 +18,10 @@ import * as common from 'enso-common'
|
||||
|
||||
import * as fileAssociations from './file-associations'
|
||||
import * as paths from './paths'
|
||||
import computeHashes from './tasks/computeHashes.mjs'
|
||||
import signArchivesMacOs from './tasks/signArchivesMacOs'
|
||||
|
||||
import BUILD_INFO from '../../build.json' assert { type: 'json' }
|
||||
import BUILD_INFO from '../../../../build.json' assert { type: 'json' }
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
@ -213,8 +213,7 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
|
||||
// https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
|
||||
sign: false,
|
||||
},
|
||||
afterAllArtifactBuild: path.join('tasks', 'computeHashes.cjs'),
|
||||
|
||||
afterAllArtifactBuild: computeHashes,
|
||||
afterPack: ctx => {
|
||||
if (passedArgs.platform === electronBuilder.Platform.MAC) {
|
||||
// Make the subtree writable, so we can sign the binaries.
|
||||
|
@ -20,7 +20,7 @@
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/opener": "^1.4.0",
|
||||
"chalk": "^5.2.0",
|
||||
"create-servers": "^3.2.0",
|
||||
"create-servers": "3.2.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"opener": "^1.5.2",
|
||||
@ -38,7 +38,7 @@
|
||||
"electron-builder": "^22.14.13",
|
||||
"electron-notarize": "1.2.2",
|
||||
"enso-common": "^1.0.0",
|
||||
"esbuild": "^0.17.15",
|
||||
"esbuild": "^0.19.3",
|
||||
"fast-glob": "^3.2.12",
|
||||
"portfinder": "^1.0.32",
|
||||
"tsx": "^3.12.6"
|
||||
@ -51,7 +51,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "npx --yes eslint src",
|
||||
"start": "tsx start.ts",
|
||||
"build": "tsx bundle.ts",
|
||||
"dist": "tsx dist.ts",
|
||||
|
@ -26,7 +26,10 @@ const DEFAULT_PORT = 8080
|
||||
export class WindowSize {
|
||||
static separator = 'x'
|
||||
/** 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. */
|
||||
static default(): WindowSize {
|
||||
|
@ -3,15 +3,15 @@
|
||||
import chalk from 'chalk'
|
||||
import stringLength from 'string-length'
|
||||
|
||||
import yargs from 'yargs/yargs'
|
||||
import yargsModule from 'yargs'
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import yargs, { Options } from 'yargs'
|
||||
|
||||
import * as contentConfig from 'enso-content-config'
|
||||
|
||||
import * as config from 'config'
|
||||
import * as fileAssociations from 'file-associations'
|
||||
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
|
||||
|
||||
@ -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. */
|
||||
export class 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. */
|
||||
display(): string {
|
||||
@ -275,20 +278,18 @@ function argvAndChromeOptions(processArgs: string[]): ArgvAndChromeOptions {
|
||||
export function parseArgs(clientArgs: string[] = fileAssociations.CLIENT_ARGUMENTS) {
|
||||
const args = config.CONFIG
|
||||
const { argv, chromeOptions } = argvAndChromeOptions(fixArgvNoPrefix(clientArgs))
|
||||
const yargsOptions = args
|
||||
.optionsRecursive()
|
||||
.reduce((opts: Record<string, yargsModule.Options>, option) => {
|
||||
opts[naming.camelToKebabCase(option.qualifiedName())] = {
|
||||
...option,
|
||||
requiresArg: ['string', 'array'].includes(option.type),
|
||||
default: null,
|
||||
// Required because yargs defines `defaultDescription`
|
||||
// as `string | undefined`, not `string | null`.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
defaultDescription: option.defaultDescription ?? undefined,
|
||||
}
|
||||
return opts
|
||||
}, {})
|
||||
const yargsOptions = args.optionsRecursive().reduce((opts: Record<string, Options>, option) => {
|
||||
opts[naming.camelToKebabCase(option.qualifiedName())] = {
|
||||
...option,
|
||||
requiresArg: ['string', 'array'].includes(option.type),
|
||||
default: null,
|
||||
// Required because yargs defines `defaultDescription`
|
||||
// as `string | undefined`, not `string | null`.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
defaultDescription: option.defaultDescription ?? undefined,
|
||||
}
|
||||
return opts
|
||||
}, {})
|
||||
|
||||
const optParser = yargs()
|
||||
.version(false)
|
||||
|
@ -1,6 +1,6 @@
|
||||
/** @file Application debug information. */
|
||||
|
||||
import BUILD_INFO from '../../../build.json' assert { type: 'json' }
|
||||
import BUILD_INFO from '../../../../../build.json' assert { type: 'json' }
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
|
@ -9,11 +9,11 @@
|
||||
import * as fsSync from 'node:fs'
|
||||
import * as pathModule from 'node:path'
|
||||
|
||||
import * as linkedDist from 'ensogl-runner/src/runner'
|
||||
|
||||
import * as contentConfig from 'enso-content-config'
|
||||
import * as paths from 'paths'
|
||||
|
||||
import * as linkedDist from '../../../../../target/ensogl-pack/linked-dist'
|
||||
|
||||
// ================
|
||||
// === Log File ===
|
||||
// ================
|
||||
|
@ -1,11 +1,8 @@
|
||||
/** @file Definition of hash computing functions. */
|
||||
|
||||
// Eslint is not (and should not be) set up to recognize CommonJS imports.
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const cryptoModule = require('crypto')
|
||||
const fs = require('fs')
|
||||
const pathModule = require('path')
|
||||
/* eslint-enable no-restricted-syntax */
|
||||
import * as cryptoModule from 'node:crypto'
|
||||
import * as fs from 'node:fs'
|
||||
import * as pathModule from 'node:path'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -63,8 +60,10 @@ async function writeFileChecksum(path, type) {
|
||||
// ================
|
||||
|
||||
/** Generates checksums for all build artifacts.
|
||||
* @param {import('electron-builder').BuildResult} context - Build information. */
|
||||
exports.default = async function (context) {
|
||||
* @param {import('electron-builder').BuildResult} context - Build information.
|
||||
* @returns {Promise<string[]>} afterAllArtifactBuild hook result.
|
||||
*/
|
||||
export default async function (context) {
|
||||
// `context` is BuildResult, see
|
||||
// https://www.electron.build/configuration/configuration.html#buildresult
|
||||
for (const file of context.artifactPaths) {
|
@ -4,5 +4,12 @@
|
||||
"baseUrl": "./src",
|
||||
"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.write = false
|
||||
const dashboardBuilder = await esbuild.context(dashboardOpts)
|
||||
const dashboard = await dashboardBuilder.rebuild()
|
||||
console.log('Result of dashboard bundling: ', dashboard)
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import * as semver from 'semver'
|
||||
|
||||
import * as linkedDist from '../../../../../target/ensogl-pack/linked-dist'
|
||||
import BUILD_INFO from '../../../build.json' assert { type: 'json' }
|
||||
import * as linkedDist from 'ensogl-runner/src/runner'
|
||||
import BUILD_INFO from '../../../../../build.json' assert { type: 'json' }
|
||||
|
||||
// Aliases with the same name as the original.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"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 esbuildPluginNodeGlobals from '@esbuild-plugins/node-globals-polyfill'
|
||||
import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill'
|
||||
import esbuildPluginAlias from 'esbuild-plugin-alias'
|
||||
import esbuildPluginCopyDirectories from 'esbuild-plugin-copy-directories'
|
||||
import esbuildPluginTime from 'esbuild-plugin-time'
|
||||
import esbuildPluginYaml from 'esbuild-plugin-yaml'
|
||||
|
||||
import * as utils from '../../utils'
|
||||
import BUILD_INFO from '../../build.json' assert { type: 'json' }
|
||||
import BUILD_INFO from '../../../../build.json' assert { type: 'json' }
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -54,8 +53,6 @@ export interface Arguments extends PassthroughArguments {
|
||||
assetsPath: string
|
||||
/** Path where bundled files are output. */
|
||||
outputPath: string
|
||||
/** The main JS bundle to load WASM and JS wasm-pack bundles. */
|
||||
ensoglAppPath: string
|
||||
}
|
||||
|
||||
/** 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 assetsPath = 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, ensoglAppPath }
|
||||
return { ...passthroughArguments, wasmArtifacts, assetsPath, outputPath }
|
||||
}
|
||||
|
||||
// ===================
|
||||
@ -89,7 +85,6 @@ function git(command: string): string {
|
||||
export function bundlerOptions(args: Arguments) {
|
||||
const {
|
||||
outputPath,
|
||||
ensoglAppPath,
|
||||
wasmArtifacts,
|
||||
assetsPath,
|
||||
devMode,
|
||||
@ -156,7 +151,6 @@ export function bundlerOptions(args: Arguments) {
|
||||
esbuildPluginYaml.yamlPlugin({}),
|
||||
esbuildPluginNodeModules.NodeModulesPolyfillPlugin(),
|
||||
esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }),
|
||||
esbuildPluginAlias({ ensogl_app: ensoglAppPath }),
|
||||
esbuildPluginTime(),
|
||||
],
|
||||
define: {
|
||||
|
@ -16,7 +16,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "npx --yes eslint src",
|
||||
"lint": "eslint src",
|
||||
"build": "tsx bundle.ts",
|
||||
"watch": "tsx watch.ts",
|
||||
"start": "tsx start.ts"
|
||||
@ -29,27 +29,26 @@
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@eslint/js": "^8.36.0",
|
||||
"@eslint/js": "^8.49.0",
|
||||
"@types/connect": "^3.4.35",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/serve-static": "^1.15.1",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/to-ico": "^1.1.1",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"esbuild": "^0.17.15",
|
||||
"esbuild-plugin-alias": "^0.2.1",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-copy-directories": "^1.0.0",
|
||||
"esbuild-plugin-time": "^1.0.0",
|
||||
"esbuild-plugin-yaml": "^0.0.1",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-plugin-jsdoc": "^40.0.2",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-jsdoc": "^46.8.1",
|
||||
"globals": "^13.20.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"tsx": "^3.12.6",
|
||||
"typescript": "^4.9.3"
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-x64": "^0.17.15",
|
||||
|
@ -4,7 +4,7 @@
|
||||
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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
@ -5,12 +5,12 @@
|
||||
import * as semver from 'semver'
|
||||
import * as toastify from 'react-toastify'
|
||||
|
||||
import * as app from 'ensogl-runner/src/runner'
|
||||
import * as common from 'enso-common'
|
||||
import * as contentConfig from 'enso-content-config'
|
||||
import * as dashboard from 'enso-authentication'
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import * as app from '../../../../../target/ensogl-pack/linked-dist'
|
||||
import * as remoteLog from './remoteLog'
|
||||
import GLOBAL_CONFIG from '../../../../gui/config.yaml' assert { type: 'yaml' }
|
||||
|
||||
@ -141,7 +141,7 @@ function displayDeprecatedVersionDialog() {
|
||||
// ========================
|
||||
|
||||
/** Nested configuration options with `string` values. */
|
||||
interface StringConfig {
|
||||
export interface StringConfig {
|
||||
[key: string]: StringConfig | string
|
||||
}
|
||||
|
||||
@ -150,9 +150,7 @@ interface AuthenticationConfig {
|
||||
projectManagerUrl: string | null
|
||||
isInAuthenticationFlow: boolean
|
||||
shouldUseAuthentication: boolean
|
||||
shouldUseNewDashboard: boolean
|
||||
initialProjectName: string | null
|
||||
inputConfig: StringConfig | null
|
||||
}
|
||||
|
||||
/** Contains the entrypoint into the IDE. */
|
||||
@ -258,8 +256,6 @@ class Main implements AppRunner {
|
||||
}
|
||||
if (parseOk) {
|
||||
const shouldUseAuthentication = configOptions.options.authentication.value
|
||||
const shouldUseNewDashboard =
|
||||
configOptions.groups.featurePreview.options.newDashboard.value
|
||||
const isOpeningMainEntryPoint =
|
||||
configOptions.groups.startup.options.entry.value ===
|
||||
configOptions.groups.startup.options.entry.default
|
||||
@ -275,14 +271,12 @@ class Main implements AppRunner {
|
||||
url.searchParams.delete('startup.project')
|
||||
history.replaceState(null, '', url.toString())
|
||||
}
|
||||
if ((shouldUseAuthentication || shouldUseNewDashboard) && isOpeningMainEntryPoint) {
|
||||
if (shouldUseAuthentication && isOpeningMainEntryPoint) {
|
||||
this.runAuthentication({
|
||||
isInAuthenticationFlow,
|
||||
projectManagerUrl,
|
||||
shouldUseAuthentication,
|
||||
shouldUseNewDashboard,
|
||||
initialProjectName,
|
||||
inputConfig: inputConfig ?? null,
|
||||
})
|
||||
} else {
|
||||
void this.runApp(inputConfig ?? null, null)
|
||||
@ -292,6 +286,12 @@ class Main implements AppRunner {
|
||||
|
||||
/** Begins the authentication UI flow. */
|
||||
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
|
||||
* `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
|
||||
@ -308,9 +308,9 @@ class Main implements AppRunner {
|
||||
supportsDeepLinks: SUPPORTS_DEEP_LINKS,
|
||||
projectManagerUrl: config.projectManagerUrl,
|
||||
isAuthenticationDisabled: !config.shouldUseAuthentication,
|
||||
shouldShowDashboard: config.shouldUseNewDashboard,
|
||||
shouldShowDashboard: true,
|
||||
initialProjectName: config.initialProjectName,
|
||||
onAuthenticated: (accessToken: string | null) => {
|
||||
onAuthenticated: () => {
|
||||
if (config.isInAuthenticationFlow) {
|
||||
const initialUrl = localStorage.getItem(INITIAL_URL_KEY)
|
||||
if (initialUrl != null) {
|
||||
@ -319,17 +319,6 @@ class Main implements AppRunner {
|
||||
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.
|
||||
* {@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'
|
||||
|
||||
const logger = app.log.logger
|
||||
|
@ -1,4 +1,10 @@
|
||||
{
|
||||
"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 ANALYZE = process.argv.includes('--analyze')
|
||||
|
||||
// ===============
|
||||
// === Bundler ===
|
||||
@ -33,8 +34,12 @@ async function bundle() {
|
||||
path.resolve(THIS_PATH, 'src', 'index.html'),
|
||||
path.resolve(THIS_PATH, 'src', 'index.tsx')
|
||||
)
|
||||
opts.metafile = ANALYZE
|
||||
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
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -12,6 +12,7 @@ import * as url from 'node:url'
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill'
|
||||
import esbuildPluginInlineImage from 'esbuild-plugin-inline-image'
|
||||
import esbuildPluginTime from 'esbuild-plugin-time'
|
||||
import esbuildPluginYaml from 'esbuild-plugin-yaml'
|
||||
|
||||
@ -19,6 +20,7 @@ import postcss from 'postcss'
|
||||
import tailwindcss from 'tailwindcss'
|
||||
import tailwindcssNesting from 'tailwindcss/nesting/index.js'
|
||||
|
||||
import * as tailwindConfig from './tailwind.config'
|
||||
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 TAILWIND_CONFIG_PATH = path.resolve(THIS_PATH, 'tailwind.config.ts')
|
||||
|
||||
// =============================
|
||||
// === Environment variables ===
|
||||
@ -51,25 +52,25 @@ export function argumentsFromEnv(): Arguments {
|
||||
// =======================
|
||||
|
||||
/** A plugin to process all CSS files with Tailwind CSS. */
|
||||
function esbuildPluginGenerateTailwind(): esbuild.Plugin {
|
||||
export function esbuildPluginGenerateTailwind(): esbuild.Plugin {
|
||||
return {
|
||||
name: 'enso-generate-tailwind',
|
||||
setup: build => {
|
||||
const cssProcessor = postcss([
|
||||
tailwindcss({
|
||||
config: TAILWIND_CONFIG_PATH,
|
||||
config: tailwindConfig,
|
||||
}),
|
||||
tailwindcssNesting(),
|
||||
])
|
||||
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 result = await cssProcessor.process(content, { from: loadArgs.path })
|
||||
console.log(`Processed CSS file '${loadArgs.path}'.`)
|
||||
// console.log(`Processed CSS file '${loadArgs.path}'.`)
|
||||
return {
|
||||
contents: result.content,
|
||||
loader: 'css',
|
||||
watchFiles: [loadArgs.path, TAILWIND_CONFIG_PATH],
|
||||
watchFiles: [loadArgs.path],
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -93,19 +94,20 @@ export function bundlerOptions(args: Arguments) {
|
||||
outdir: outputPath,
|
||||
outbase: 'src',
|
||||
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 */
|
||||
'.svg': 'dataurl',
|
||||
// The `file` loader copies the file, and replaces the import with the path to the file.
|
||||
'.png': 'file',
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
},
|
||||
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(),
|
||||
esbuildPluginTime(),
|
||||
// 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 */
|
||||
},
|
||||
pure: ['assert'],
|
||||
sourcemap: trueBoolean,
|
||||
sourcemap: true,
|
||||
minify: !devMode,
|
||||
metafile: trueBoolean,
|
||||
format: 'esm',
|
||||
|
@ -4,8 +4,8 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "npx --yes eslint src",
|
||||
"typecheck": "tsc",
|
||||
"lint": "eslint src",
|
||||
"build": "tsx bundle.ts",
|
||||
"watch": "tsx watch.ts",
|
||||
"start": "tsx start.ts",
|
||||
@ -13,10 +13,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.15",
|
||||
"@types/node": "^16.18.11",
|
||||
"@types/node": "^18.17.5",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"esbuild": "^0.17.15",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-time": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -24,18 +24,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"chalk": "^5.3.0",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-chat": "git://github.com/enso-org/enso-bot",
|
||||
"enso-content": "^1.0.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.8",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-jsdoc": "^46.8.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "~5.2.2",
|
||||
"tsx": "^3.12.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-x64": "^0.17.15",
|
||||
|
@ -5,15 +5,17 @@
|
||||
"main": "./src/index.tsx",
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./src/platform": "./src/platform.ts"
|
||||
"./src/platform": "./src/platform.ts",
|
||||
"./tailwind.css": "./tailwind.css"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-amplify/auth": "^5.1.8",
|
||||
"@aws-amplify/core": "^5.0.14",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||
"@aws-amplify/auth": "^5.6.5",
|
||||
"@aws-amplify/core": "^5.8.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"enso-common": "^1.0.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
@ -21,6 +23,10 @@
|
||||
"ts-results": "^3.3.0"
|
||||
},
|
||||
"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
|
||||
* 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}
|
||||
* map lists the Amplify errors that we want to catch and convert to typed responses.
|
||||
* that can be resolved must be caught and handled as early as possible.
|
||||
*
|
||||
* # Handling Amplify Errors
|
||||
*
|
||||
* 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
|
||||
* from `unknown` to a typed object. Then, use the {@link KNOWN_ERRORS} to see if the error is one
|
||||
* that must be handled by the application (i.e., it is an error that is relevant to our business
|
||||
* logic). */
|
||||
* from `unknown` to a typed object. Then, use one of the response error handling functions (e.g.
|
||||
* {@link intoSignUpErrorOrThrow}) to see if the error is one that must be handled by the
|
||||
* application (i.e., it is an error that is relevant to our business logic). */
|
||||
interface AmplifyError extends Error {
|
||||
/** Error code for disambiguating the error. */
|
||||
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. */
|
||||
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.
|
||||
* @throws {Error} If the error is not recognized. */
|
||||
*
|
||||
* @throws {Error} If the error is not recognized.
|
||||
*/
|
||||
function intoCurrentSessionErrorKind(error: unknown): CurrentSessionErrorKind {
|
||||
if (error === CURRENT_SESSION_NO_CURRENT_USER_ERROR.internalMessage) {
|
||||
return CURRENT_SESSION_NO_CURRENT_USER_ERROR.kind
|
||||
@ -467,9 +469,12 @@ export interface SignUpError extends CognitoError {
|
||||
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.
|
||||
* @throws {Error} If the error is not recognized. */
|
||||
*
|
||||
* @throws {Error} If the error is not recognized.
|
||||
*/
|
||||
function intoSignUpErrorOrThrow(error: AmplifyError): SignUpError {
|
||||
if (error.code === SIGN_UP_USERNAME_EXISTS_ERROR.internalCode) {
|
||||
return {
|
||||
|
@ -42,12 +42,12 @@ function isAuthEvent(value: string): value is AuthEvent {
|
||||
|
||||
/** 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
|
||||
|
||||
/** Unsubscribe the {@link ListenerCallback} from authentication state changes.
|
||||
*
|
||||
* @see {@link Api["listen"]}. */
|
||||
* @see {@link amplify.Hub.listen}. */
|
||||
type UnsubscribeFunction = () => void
|
||||
|
||||
/** 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
|
||||
* 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 {
|
||||
goOffline: (shouldShowToast?: boolean) => 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 */
|
||||
|
||||
// ========================
|
||||
// === 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 ===
|
||||
// =============
|
||||
@ -356,7 +326,7 @@ export interface SimpleUser {
|
||||
/** User permission for a specific user. */
|
||||
export interface UserPermission {
|
||||
user: User
|
||||
permission: PermissionAction
|
||||
permission: permissions.PermissionAction
|
||||
}
|
||||
|
||||
/** The type returned from the "update directory" endpoint. */
|
||||
@ -571,7 +541,7 @@ export interface InviteUserRequestBody {
|
||||
export interface CreatePermissionRequestBody {
|
||||
userSubjects: Subject[]
|
||||
resourceId: AssetId
|
||||
action: PermissionAction | null
|
||||
action: permissions.PermissionAction | null
|
||||
}
|
||||
|
||||
/** 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 dateTime from './dateTime'
|
||||
import * as modalProvider from '../providers/modal'
|
||||
import * as permissions from './permissions'
|
||||
import * as sorting from './sorting'
|
||||
import * as tableColumn from './components/tableColumn'
|
||||
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}`,
|
||||
} 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<
|
||||
assetTreeNode.AssetTreeNode,
|
||||
assetsTable.AssetsTableState,
|
||||
@ -165,8 +166,8 @@ function SharedWithColumn(props: AssetColumnProps) {
|
||||
permission => permission.user.user_email === session.organization?.email
|
||||
)
|
||||
const managesThisAsset =
|
||||
self?.permission === backend.PermissionAction.own ||
|
||||
self?.permission === backend.PermissionAction.admin
|
||||
self?.permission === permissions.PermissionAction.own ||
|
||||
self?.permission === permissions.PermissionAction.admin
|
||||
const setAsset = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<backend.AnyAsset>) => {
|
||||
if (typeof valueOrUpdater === 'function') {
|
||||
|
@ -7,6 +7,7 @@ import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as backendModule from '../backend'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as http from '../../http'
|
||||
import * as permissions from '../permissions'
|
||||
import * as remoteBackendModule from '../remoteBackend'
|
||||
import * as shortcuts from '../shortcuts'
|
||||
|
||||
@ -67,14 +68,14 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
|
||||
)
|
||||
const managesThisAsset =
|
||||
backend.type === backendModule.BackendType.local ||
|
||||
self?.permission === backendModule.PermissionAction.own ||
|
||||
self?.permission === backendModule.PermissionAction.admin
|
||||
self?.permission === permissions.PermissionAction.own ||
|
||||
self?.permission === permissions.PermissionAction.admin
|
||||
const isRunningProject =
|
||||
asset.type === backendModule.AssetType.project &&
|
||||
backendModule.DOES_PROJECT_STATE_INDICATE_VM_EXISTS[asset.projectState.type]
|
||||
const canExecute =
|
||||
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 =
|
||||
backend.type !== backendModule.BackendType.local &&
|
||||
backendModule.assetIsProject(asset) &&
|
||||
|
@ -5,8 +5,6 @@ import * as backendModule from '../backend'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as load from '../load'
|
||||
|
||||
import GLOBAL_CONFIG from '../../../../../../../../gui/config.yaml' assert { type: 'yaml' }
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
@ -114,15 +112,10 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
}
|
||||
const runNewProject = async () => {
|
||||
const engineConfig =
|
||||
backendType === backendModule.BackendType.remote
|
||||
? {
|
||||
rpcUrl: jsonAddress,
|
||||
dataUrl: binaryAddress,
|
||||
}
|
||||
: {
|
||||
projectManagerUrl: GLOBAL_CONFIG.projectManagerEndpoint,
|
||||
}
|
||||
const engineConfig = {
|
||||
rpcUrl: jsonAddress,
|
||||
dataUrl: binaryAddress,
|
||||
}
|
||||
const originalUrl = window.location.href
|
||||
if (backendType === backendModule.BackendType.remote) {
|
||||
// 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 hooks from '../../hooks'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as permissionsModule from '../permissions'
|
||||
|
||||
import Autocomplete from './autocomplete'
|
||||
import Modal from './modal'
|
||||
@ -27,7 +28,7 @@ const TYPE_SELECTOR_Y_OFFSET_PX = 32
|
||||
|
||||
/** Props for a {@link ManagePermissionsModal}. */
|
||||
export interface ManagePermissionsModalProps<
|
||||
Asset extends backendModule.AnyAsset = backendModule.AnyAsset
|
||||
Asset extends backendModule.AnyAsset = backendModule.AnyAsset,
|
||||
> {
|
||||
item: 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.
|
||||
* This should never happen, as this modal should not be accessible in either case. */
|
||||
export default function ManagePermissionsModal<
|
||||
Asset extends backendModule.AnyAsset = backendModule.AnyAsset
|
||||
Asset extends backendModule.AnyAsset = backendModule.AnyAsset,
|
||||
>(props: ManagePermissionsModalProps<Asset>) {
|
||||
const { item, setItem, self, doRemoveSelf, eventTarget } = props
|
||||
const { organization } = auth.useNonPartialUserSession()
|
||||
@ -53,15 +54,15 @@ export default function ManagePermissionsModal<
|
||||
const [permissions, setPermissions] = React.useState(item.permissions ?? [])
|
||||
const [users, setUsers] = React.useState<backendModule.SimpleUser[]>([])
|
||||
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 position = React.useMemo(() => eventTarget?.getBoundingClientRect(), [eventTarget])
|
||||
const editablePermissions = React.useMemo(
|
||||
() =>
|
||||
self.permission === backendModule.PermissionAction.own
|
||||
self.permission === permissionsModule.PermissionAction.own
|
||||
? permissions
|
||||
: permissions.filter(
|
||||
permission => permission.permission !== backendModule.PermissionAction.own
|
||||
permission => permission.permission !== permissionsModule.PermissionAction.own
|
||||
),
|
||||
[permissions, self.permission]
|
||||
)
|
||||
@ -78,10 +79,10 @@ export default function ManagePermissionsModal<
|
||||
)
|
||||
const isOnlyOwner = React.useMemo(
|
||||
() =>
|
||||
self.permission === backendModule.PermissionAction.own &&
|
||||
self.permission === permissionsModule.PermissionAction.own &&
|
||||
permissions.every(
|
||||
permission =>
|
||||
permission.permission !== backendModule.PermissionAction.own ||
|
||||
permission.permission !== permissionsModule.PermissionAction.own ||
|
||||
permission.user.user_email === organization?.email
|
||||
),
|
||||
[organization?.email, permissions, self.permission]
|
||||
@ -278,7 +279,7 @@ export default function ManagePermissionsModal<
|
||||
disabled={willInviteNewUser}
|
||||
selfPermission={self.permission}
|
||||
typeSelectorYOffsetPx={TYPE_SELECTOR_Y_OFFSET_PX}
|
||||
action={backendModule.PermissionAction.view}
|
||||
action={permissionsModule.PermissionAction.view}
|
||||
assetType={item.type}
|
||||
onChange={setAction}
|
||||
/>
|
||||
|
@ -1,7 +1,6 @@
|
||||
/** @file Colored border around icons and text indicating permissions. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backend from '../backend'
|
||||
import * as permissionsModule from '../permissions'
|
||||
|
||||
// =================
|
||||
@ -10,7 +9,7 @@ import * as permissionsModule from '../permissions'
|
||||
|
||||
/** Props for a {@link PermissionDisplay}. */
|
||||
export interface PermissionDisplayProps extends React.PropsWithChildren {
|
||||
action: backend.PermissionAction
|
||||
action: permissionsModule.PermissionAction
|
||||
className?: string
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backend from '../backend'
|
||||
import * as permissions from '../permissions'
|
||||
import * as permissionsModule from '../permissions'
|
||||
|
||||
import Modal from './modal'
|
||||
@ -34,12 +35,12 @@ export interface PermissionSelectorProps {
|
||||
/** Overrides the vertical offset of the {@link PermissionTypeSelector}. */
|
||||
typeSelectorYOffsetPx?: number
|
||||
error?: string | null
|
||||
selfPermission: backend.PermissionAction
|
||||
selfPermission: permissions.PermissionAction
|
||||
/** If this prop changes, the internal state will be updated too. */
|
||||
action: backend.PermissionAction
|
||||
action: permissions.PermissionAction
|
||||
assetType: backend.AssetType
|
||||
className?: string
|
||||
onChange: (action: backend.PermissionAction) => void
|
||||
onChange: (action: permissions.PermissionAction) => void
|
||||
doDelete?: () => void
|
||||
}
|
||||
|
||||
@ -61,7 +62,7 @@ export default function PermissionSelector(props: PermissionSelectorProps) {
|
||||
const [TheChild, setTheChild] = React.useState<(() => JSX.Element) | null>()
|
||||
const permission = permissionsModule.FROM_PERMISSION_ACTION[action]
|
||||
|
||||
const setAction = (newAction: backend.PermissionAction) => {
|
||||
const setAction = (newAction: permissions.PermissionAction) => {
|
||||
setActionRaw(newAction)
|
||||
onChange(newAction)
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ const PERMISSION_TYPE_DATA: PermissionTypeData[] = [
|
||||
/** Props for a {@link PermissionTypeSelector}. */
|
||||
export interface PermissionTypeSelectorProps {
|
||||
showDelete?: boolean
|
||||
selfPermission: backend.PermissionAction
|
||||
selfPermission: permissions.PermissionAction
|
||||
type: permissions.Permission
|
||||
assetType: backend.AssetType
|
||||
style?: React.CSSProperties
|
||||
@ -93,7 +93,7 @@ export default function PermissionTypeSelector(props: PermissionTypeSelectorProp
|
||||
{PERMISSION_TYPE_DATA.filter(
|
||||
data =>
|
||||
(showDelete ? true : data.type !== permissions.Permission.delete) &&
|
||||
(selfPermission === backend.PermissionAction.own
|
||||
(selfPermission === permissions.PermissionAction.own
|
||||
? true
|
||||
: data.type !== permissions.Permission.owner)
|
||||
).map(data => (
|
||||
|
@ -13,6 +13,7 @@ import * as errorModule from '../../error'
|
||||
import * as eventModule from '../event'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as indent from '../indent'
|
||||
import * as permissions from '../permissions'
|
||||
import * as presence from '../presence'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
@ -66,7 +67,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
const canExecute =
|
||||
backend.type === backendModule.BackendType.local ||
|
||||
(ownPermission != null &&
|
||||
backendModule.PERMISSION_ACTION_CAN_EXECUTE[ownPermission.permission])
|
||||
permissions.PERMISSION_ACTION_CAN_EXECUTE[ownPermission.permission])
|
||||
const isOtherUserUsingProject =
|
||||
backend.type !== backendModule.BackendType.local &&
|
||||
asset.projectState.opened_by != null &&
|
||||
|
@ -77,7 +77,7 @@ export type TableProps<
|
||||
T,
|
||||
State = never,
|
||||
RowState = never,
|
||||
Key extends string = string
|
||||
Key extends string = string,
|
||||
> = InternalTableProps<T, State, RowState, Key> &
|
||||
([RowState] extends [never] ? unknown : InitialRowStateProp<RowState>) &
|
||||
([State] extends [never] ? unknown : StateProp<State>) &
|
||||
|
@ -4,7 +4,7 @@
|
||||
// === Types ===
|
||||
// =============
|
||||
|
||||
/** Props for a {@link Column}. */
|
||||
/** Props for a {@link TableColumn}. */
|
||||
export interface TableColumnProps<T, State = never, RowState = never, Key extends string = string> {
|
||||
keyProp: Key
|
||||
item: T
|
||||
@ -16,7 +16,7 @@ export interface TableColumnProps<T, State = never, RowState = never, Key extend
|
||||
setRowState: React.Dispatch<React.SetStateAction<RowState>>
|
||||
}
|
||||
|
||||
/** Props for a {@link Column}. */
|
||||
/** Props for a {@link TableColumn}. */
|
||||
export interface TableColumnHeadingProps<State = never> {
|
||||
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