diff options
Diffstat (limited to 'subprojects/frontend')
52 files changed, 960 insertions, 899 deletions
diff --git a/subprojects/frontend/.eslintrc.cjs b/subprojects/frontend/.eslintrc.cjs new file mode 100644 index 00000000..e6be4d65 --- /dev/null +++ b/subprojects/frontend/.eslintrc.cjs | |||
@@ -0,0 +1,89 @@ | |||
1 | const path = require('node:path'); | ||
2 | |||
3 | // Allow the Codium ESLint plugin to find `tsconfig.json` from the repository root. | ||
4 | const project = [ | ||
5 | path.join(__dirname, 'tsconfig.json'), | ||
6 | path.join(__dirname, 'tsconfig.node.json'), | ||
7 | ]; | ||
8 | |||
9 | /** @type {import('eslint').Linter.Config} */ | ||
10 | module.exports = { | ||
11 | plugins: ['@typescript-eslint'], | ||
12 | extends: [ | ||
13 | 'airbnb', | ||
14 | 'airbnb-typescript', | ||
15 | 'airbnb/hooks', | ||
16 | 'plugin:@typescript-eslint/recommended', | ||
17 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', | ||
18 | 'plugin:prettier/recommended', | ||
19 | ], | ||
20 | parserOptions: { | ||
21 | project, | ||
22 | sourceType: 'module', | ||
23 | }, | ||
24 | parser: '@typescript-eslint/parser', | ||
25 | settings: { | ||
26 | 'import/parsers': { | ||
27 | '@typescript-eslint/parser': ['.ts', '.tsx'], | ||
28 | }, | ||
29 | 'import/resolver': { | ||
30 | typescript: { | ||
31 | alwaysTryTypes: true, | ||
32 | project, | ||
33 | }, | ||
34 | }, | ||
35 | }, | ||
36 | env: { | ||
37 | browser: true, | ||
38 | }, | ||
39 | ignorePatterns: ['build/**/*'], | ||
40 | rules: { | ||
41 | // In typescript, some class methods implementing an inderface do not use `this`: | ||
42 | // https://github.com/typescript-eslint/typescript-eslint/issues/1103 | ||
43 | 'class-methods-use-this': 'off', | ||
44 | // Make sure every import can be resolved by `eslint-import-resolver-typescript`. | ||
45 | 'import/no-unresolved': 'error', | ||
46 | // Organize imports automatically. | ||
47 | 'import/order': [ | ||
48 | 'error', | ||
49 | { | ||
50 | alphabetize: { | ||
51 | order: 'asc', | ||
52 | }, | ||
53 | 'newlines-between': 'always', | ||
54 | }, | ||
55 | ], | ||
56 | }, | ||
57 | overrides: [ | ||
58 | { | ||
59 | files: ['types/**/*.d.ts'], | ||
60 | rules: { | ||
61 | // We don't have control over exports of external modules. | ||
62 | 'import/prefer-default-export': 'off', | ||
63 | }, | ||
64 | }, | ||
65 | { | ||
66 | files: ['*.cjs'], | ||
67 | rules: { | ||
68 | // https://github.com/typescript-eslint/typescript-eslint/issues/1724 | ||
69 | '@typescript-eslint/no-var-requires': 'off', | ||
70 | }, | ||
71 | }, | ||
72 | { | ||
73 | files: ['.eslintrc.cjs', 'prettier.config.cjs', 'vite.config.ts'], | ||
74 | env: { | ||
75 | browser: false, | ||
76 | node: true, | ||
77 | }, | ||
78 | rules: { | ||
79 | // Allow devDependencies in configuration files. | ||
80 | 'import/no-extraneous-dependencies': [ | ||
81 | 'error', | ||
82 | { devDependencies: true }, | ||
83 | ], | ||
84 | // Access to the environment in configuration files. | ||
85 | 'no-process-env': 'off', | ||
86 | }, | ||
87 | }, | ||
88 | ], | ||
89 | }; | ||
diff --git a/subprojects/frontend/.eslintrc.js b/subprojects/frontend/.eslintrc.js deleted file mode 100644 index aa7636f8..00000000 --- a/subprojects/frontend/.eslintrc.js +++ /dev/null | |||
@@ -1,39 +0,0 @@ | |||
1 | // Loosely based on | ||
2 | // https://github.com/iamturns/create-exposed-app/blob/f14e435b8ce179c89cce3eea89e56202153a53da/.eslintrc.js | ||
3 | module.exports = { | ||
4 | plugins: [ | ||
5 | '@typescript-eslint', | ||
6 | ], | ||
7 | extends: [ | ||
8 | 'airbnb', | ||
9 | 'airbnb-typescript', | ||
10 | 'airbnb/hooks', | ||
11 | 'plugin:@typescript-eslint/recommended', | ||
12 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', | ||
13 | ], | ||
14 | parserOptions: { | ||
15 | project: './tsconfig.json', | ||
16 | }, | ||
17 | rules: { | ||
18 | // https://basarat.gitbooks.io/typescript/docs/tips/defaultIsBad.html | ||
19 | 'import/prefer-default-export': 'off', | ||
20 | 'import/no-default-export': 'error', | ||
21 | // propTypes are for runtime validation, but we rely on TypeScript for build-time validation: | ||
22 | // https://github.com/yannickcr/eslint-plugin-react/issues/2275#issuecomment-492003857 | ||
23 | 'react/prop-types': 'off', | ||
24 | // Make sure switches are exhaustive: https://stackoverflow.com/a/60166264 | ||
25 | 'default-case': 'off', | ||
26 | '@typescript-eslint/switch-exhaustiveness-check': 'error', | ||
27 | // https://github.com/airbnb/javascript/pull/2501 | ||
28 | 'react/function-component-definition': ['error', { | ||
29 | namedComponents: 'function-declaration', | ||
30 | }], | ||
31 | }, | ||
32 | env: { | ||
33 | browser: true, | ||
34 | }, | ||
35 | ignorePatterns: [ | ||
36 | '*.js', | ||
37 | 'build/**/*', | ||
38 | ], | ||
39 | }; | ||
diff --git a/subprojects/frontend/.stylelintrc.js b/subprojects/frontend/.stylelintrc.js deleted file mode 100644 index 7adf8f26..00000000 --- a/subprojects/frontend/.stylelintrc.js +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | module.exports = { | ||
2 | extends: 'stylelint-config-recommended-scss', | ||
3 | // Simplified for only :export to TypeScript based on | ||
4 | // https://github.com/pascalduez/stylelint-config-css-modules/blob/d792a6ac7d2bce8239edccbc5a72e0616f22d696/index.js | ||
5 | rules: { | ||
6 | 'selector-pseudo-class-no-unknown': [ | ||
7 | true, | ||
8 | { | ||
9 | ignorePseudoClasses: [ | ||
10 | 'export', | ||
11 | ], | ||
12 | }, | ||
13 | ], | ||
14 | }, | ||
15 | }; | ||
diff --git a/subprojects/frontend/build.gradle b/subprojects/frontend/build.gradle index 71444e89..5ed90c31 100644 --- a/subprojects/frontend/build.gradle +++ b/subprojects/frontend/build.gradle | |||
@@ -5,11 +5,11 @@ plugins { | |||
5 | 5 | ||
6 | import org.siouan.frontendgradleplugin.infrastructure.gradle.RunYarn | 6 | import org.siouan.frontendgradleplugin.infrastructure.gradle.RunYarn |
7 | 7 | ||
8 | def webpackOutputDir = "${buildDir}/webpack" | 8 | def viteOutputDir = "${buildDir}/vite" |
9 | def productionResources = file("${webpackOutputDir}/production") | 9 | def productionResources = file("${viteOutputDir}/production") |
10 | 10 | ||
11 | frontend { | 11 | frontend { |
12 | assembleScript = 'assemble:webpack' | 12 | assembleScript = 'run build' |
13 | } | 13 | } |
14 | 14 | ||
15 | configurations { | 15 | configurations { |
@@ -21,23 +21,10 @@ configurations { | |||
21 | 21 | ||
22 | def installFrontend = tasks.named('installFrontend') | 22 | def installFrontend = tasks.named('installFrontend') |
23 | 23 | ||
24 | def generateLezerGrammar = tasks.register('generateLezerGrammar', RunYarn) { | ||
25 | dependsOn installFrontend | ||
26 | inputs.file 'src/language/problem.grammar' | ||
27 | inputs.file 'package.json' | ||
28 | inputs.file rootProject.file('yarn.lock') | ||
29 | outputs.file "${buildDir}/generated/sources/lezer/problem.ts" | ||
30 | outputs.file "${buildDir}/generated/sources/lezer/problem.terms.ts" | ||
31 | script = 'run assemble:lezer' | ||
32 | } | ||
33 | |||
34 | def assembleFrontend = tasks.named('assembleFrontend') | 24 | def assembleFrontend = tasks.named('assembleFrontend') |
35 | assembleFrontend.configure { | 25 | assembleFrontend.configure { |
36 | dependsOn generateLezerGrammar | ||
37 | inputs.dir 'src' | 26 | inputs.dir 'src' |
38 | inputs.file "${buildDir}/generated/sources/lezer/problem.ts" | 27 | inputs.files('package.json', 'tsconfig.json', 'tsconfig.base.json', 'vite.config.ts') |
39 | inputs.file "${buildDir}/generated/sources/lezer/problem.terms.ts" | ||
40 | inputs.files('package.json', 'webpack.config.js') | ||
41 | inputs.file rootProject.file('yarn.lock') | 28 | inputs.file rootProject.file('yarn.lock') |
42 | outputs.dir productionResources | 29 | outputs.dir productionResources |
43 | } | 30 | } |
@@ -48,54 +35,62 @@ artifacts { | |||
48 | } | 35 | } |
49 | } | 36 | } |
50 | 37 | ||
51 | def eslint = tasks.register('eslint', RunYarn) { | 38 | def typecheckFrontend = tasks.register('typecheckFrontend', RunYarn) { |
52 | dependsOn installFrontend | 39 | dependsOn installFrontend |
53 | inputs.dir 'src' | 40 | inputs.dir 'src' |
54 | inputs.files('.eslintrc.js', 'tsconfig.json') | 41 | inputs.files('package.json', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.node.json') |
42 | inputs.file rootProject.file('yarn.lock') | ||
43 | outputs.dir "${buildDir}/typescript" | ||
44 | script = 'run typecheck' | ||
45 | group = 'verification' | ||
46 | description = 'Check for TypeScript type errors.' | ||
47 | } | ||
48 | |||
49 | def lintFrontend = tasks.register('lintFrontend', RunYarn) { | ||
50 | dependsOn installFrontend | ||
51 | inputs.dir 'src' | ||
52 | inputs.files('.eslintrc.cjs', 'prettier.config.cjs') | ||
53 | inputs.files('package.json', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.node.json') | ||
55 | inputs.file rootProject.file('yarn.lock') | 54 | inputs.file rootProject.file('yarn.lock') |
56 | if (project.hasProperty('ci')) { | 55 | if (project.hasProperty('ci')) { |
57 | outputs.file "${buildDir}/eslint.json" | 56 | outputs.file "${buildDir}/eslint.json" |
58 | script = 'run check:eslint:ci' | 57 | script = 'run lint:ci' |
59 | } else { | 58 | } else { |
60 | script = 'run check:eslint' | 59 | script = 'run lint' |
61 | } | 60 | } |
62 | group = 'verification' | 61 | group = 'verification' |
63 | description = 'Check for TypeScript errors.' | 62 | description = 'Check for TypeScript lint errors and warnings.' |
64 | } | 63 | } |
65 | 64 | ||
66 | def stylelint = tasks.register('stylelint', RunYarn) { | 65 | def prettier = tasks.register('fixFrontend', RunYarn) { |
67 | dependsOn installFrontend | 66 | dependsOn installFrontend |
68 | inputs.dir 'src' | 67 | inputs.dir 'src' |
69 | inputs.file '.stylelintrc.js' | 68 | inputs.files('.eslintrc.cjs', 'prettier.config.cjs') |
69 | inputs.files('package.json', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.node.json') | ||
70 | inputs.file rootProject.file('yarn.lock') | 70 | inputs.file rootProject.file('yarn.lock') |
71 | if (project.hasProperty('ci')) { | 71 | script = 'run lint:fix' |
72 | outputs.file "${buildDir}/stylelint.json" | ||
73 | script = 'run check:stylelint:ci' | ||
74 | } else { | ||
75 | script = 'run check:stylelint' | ||
76 | } | ||
77 | group = 'verification' | 72 | group = 'verification' |
78 | description = 'Check for Sass errors.' | 73 | description = 'Fix TypeScript lint errors and warnings.' |
79 | } | 74 | } |
80 | 75 | ||
81 | tasks.named('check') { | 76 | tasks.named('check') { |
82 | dependsOn(eslint, stylelint) | 77 | dependsOn(typecheckFrontend) |
78 | dependsOn(lintFrontend) | ||
83 | } | 79 | } |
84 | 80 | ||
85 | tasks.register('webpackServe', RunYarn) { | 81 | tasks.register('serveFrontend', RunYarn) { |
86 | dependsOn installFrontend | 82 | dependsOn installFrontend |
87 | dependsOn generateLezerGrammar | 83 | inputs.dir 'src' |
88 | outputs.dir "${webpackOutputDir}/development" | 84 | inputs.files('package.json', 'tsconfig.json', 'tsconfig.base.json', 'vite.config.ts') |
85 | inputs.file rootProject.file('yarn.lock') | ||
86 | outputs.dir "${viteOutputDir}/development" | ||
89 | script = 'run serve' | 87 | script = 'run serve' |
90 | group = 'run' | 88 | group = 'run' |
91 | description = 'Start a Webpack dev server with hot module replacement.' | 89 | description = 'Start a Vite dev server with hot module replacement.' |
92 | } | 90 | } |
93 | 91 | ||
94 | sonarqube.properties { | 92 | sonarqube.properties { |
95 | properties['sonar.sources'] += ['src'] | 93 | properties['sonar.sources'] += ['src'] |
96 | property 'sonar.nodejs.executable', "${frontend.nodeInstallDirectory.get()}/bin/node" | 94 | property 'sonar.nodejs.executable', "${frontend.nodeInstallDirectory.get()}/bin/node" |
97 | property 'sonar.eslint.reportPaths', "${buildDir}/eslint.json" | 95 | property 'sonar.eslint.reportPaths', "${buildDir}/eslint.json" |
98 | property 'sonar.css.stylelint.reportPaths', "${buildDir}/stylelint.json" | ||
99 | // SonarJS does not pick up typescript files with `exactOptionalPropertyTypes` | ||
100 | property 'sonar.typescript.tsconfigPath', 'tsconfig.sonar.json' | ||
101 | } | 96 | } |
diff --git a/subprojects/frontend/src/index.html b/subprojects/frontend/index.html index f404aa8a..999e69a3 100644 --- a/subprojects/frontend/src/index.html +++ b/subprojects/frontend/index.html | |||
@@ -12,5 +12,6 @@ | |||
12 | </p> | 12 | </p> |
13 | </noscript> | 13 | </noscript> |
14 | <div id="app"></div> | 14 | <div id="app"></div> |
15 | <script src="./src/index.tsx" type="module"></script> | ||
15 | </body> | 16 | </body> |
16 | </html> | 17 | </html> |
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 12aff7bf..59a042b3 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json | |||
@@ -2,16 +2,14 @@ | |||
2 | "name": "@refinery/frontend", | 2 | "name": "@refinery/frontend", |
3 | "version": "0.0.0", | 3 | "version": "0.0.0", |
4 | "description": "Web frontend for Refinery", | 4 | "description": "Web frontend for Refinery", |
5 | "main": "index.js", | 5 | "prive": true, |
6 | "scripts": { | 6 | "scripts": { |
7 | "assemble:lezer": "lezer-generator src/language/problem.grammar -o build/generated/sources/lezer/problem.ts", | 7 | "build": "cross-env MODE=production vite build", |
8 | "assemble:webpack": "webpack --node-env production", | 8 | "serve": "cross-env MODE=development vite serve", |
9 | "serve": "webpack serve --node-env development --hot", | 9 | "typecheck": "tsc -p tsconfig.node.json && tsc -p tsconfig.json", |
10 | "check": "yarn run check:eslint && yarn run check:stylelint", | 10 | "lint": "eslint .", |
11 | "check:eslint": "eslint .", | 11 | "lint:ci": "eslint -f json -o build/eslint.json .", |
12 | "check:eslint:ci": "eslint -f json -o build/eslint.json .", | 12 | "lint:fix": "yarn run lint --fix" |
13 | "check:stylelint": "stylelint src/**/*.scss", | ||
14 | "check:stylelint:ci": "stylelint -f json src/**/*.scss > build/stylelint.json" | ||
15 | }, | 13 | }, |
16 | "repository": { | 14 | "repository": { |
17 | "type": "git", | 15 | "type": "git", |
@@ -23,47 +21,7 @@ | |||
23 | "url": "https://github.com/graphs4value/issues" | 21 | "url": "https://github.com/graphs4value/issues" |
24 | }, | 22 | }, |
25 | "homepage": "https://refinery.tools", | 23 | "homepage": "https://refinery.tools", |
26 | "devDependencies": { | ||
27 | "@babel/core": "^7.18.10", | ||
28 | "@babel/plugin-transform-runtime": "^7.18.10", | ||
29 | "@babel/preset-env": "^7.18.10", | ||
30 | "@babel/preset-react": "^7.18.6", | ||
31 | "@babel/preset-typescript": "^7.18.6", | ||
32 | "@lezer/generator": "^1.1.1", | ||
33 | "@principalstudio/html-webpack-inject-preload": "^1.2.7", | ||
34 | "@types/react": "^18.0.17", | ||
35 | "@types/react-dom": "^18.0.6", | ||
36 | "@typescript-eslint/eslint-plugin": "^5.33.0", | ||
37 | "@typescript-eslint/parser": "^5.33.0", | ||
38 | "babel-loader": "^8.2.5", | ||
39 | "css-loader": "^6.7.1", | ||
40 | "eslint": "^8.21.0", | ||
41 | "eslint-config-airbnb": "^19.0.4", | ||
42 | "eslint-config-airbnb-typescript": "^17.0.0", | ||
43 | "eslint-import-resolver-node": "^0.3.6", | ||
44 | "eslint-plugin-import": "^2.26.0", | ||
45 | "eslint-plugin-jsx-a11y": "^6.6.1", | ||
46 | "eslint-plugin-react": "^7.30.1", | ||
47 | "eslint-plugin-react-hooks": "^4.6.0", | ||
48 | "html-webpack-plugin": "^5.5.0", | ||
49 | "image-webpack-loader": "^8.1.0", | ||
50 | "mini-css-extract-plugin": "^2.6.1", | ||
51 | "postcss": "^8.4.16", | ||
52 | "postcss-scss": "^4.0.4", | ||
53 | "sass": "^1.54.4", | ||
54 | "sass-loader": "^13.0.2", | ||
55 | "style-loader": "^3.3.1", | ||
56 | "stylelint": "^14.10.0", | ||
57 | "stylelint-config-recommended-scss": "^7.0.0", | ||
58 | "stylelint-scss": "^4.3.0", | ||
59 | "typescript": "~4.7.4", | ||
60 | "webpack": "^5.74.0", | ||
61 | "webpack-cli": "^4.10.0", | ||
62 | "webpack-dev-server": "^4.10.0", | ||
63 | "webpack-subresource-integrity": "^5.1.0" | ||
64 | }, | ||
65 | "dependencies": { | 24 | "dependencies": { |
66 | "@babel/runtime": "^7.18.9", | ||
67 | "@codemirror/autocomplete": "^6.1.0", | 25 | "@codemirror/autocomplete": "^6.1.0", |
68 | "@codemirror/commands": "^6.0.1", | 26 | "@codemirror/commands": "^6.0.1", |
69 | "@codemirror/language": "^6.2.1", | 27 | "@codemirror/language": "^6.2.1", |
@@ -77,7 +35,7 @@ | |||
77 | "@fontsource/roboto": "^4.5.8", | 35 | "@fontsource/roboto": "^4.5.8", |
78 | "@lezer/common": "^1.0.0", | 36 | "@lezer/common": "^1.0.0", |
79 | "@lezer/highlight": "^1.0.0", | 37 | "@lezer/highlight": "^1.0.0", |
80 | "@lezer/lr": "^1.2.1", | 38 | "@lezer/lr": "^1.2.2", |
81 | "@mui/icons-material": "5.8.4", | 39 | "@mui/icons-material": "5.8.4", |
82 | "@mui/material": "5.10.0", | 40 | "@mui/material": "5.10.0", |
83 | "ansi-styles": "^6.1.0", | 41 | "ansi-styles": "^6.1.0", |
@@ -90,5 +48,32 @@ | |||
90 | "react": "^18.2.0", | 48 | "react": "^18.2.0", |
91 | "react-dom": "^18.2.0", | 49 | "react-dom": "^18.2.0", |
92 | "zod": "^3.18.0" | 50 | "zod": "^3.18.0" |
51 | }, | ||
52 | "devDependencies": { | ||
53 | "@lezer/generator": "^1.1.1", | ||
54 | "@types/eslint": "^8.4.5", | ||
55 | "@types/node": "^18.7.2", | ||
56 | "@types/prettier": "^2.7.0", | ||
57 | "@types/react": "^18.0.17", | ||
58 | "@types/react-dom": "^18.0.6", | ||
59 | "@typescript-eslint/eslint-plugin": "^5.33.0", | ||
60 | "@typescript-eslint/parser": "^5.33.0", | ||
61 | "@vitejs/plugin-react": "^2.0.1", | ||
62 | "cross-env": "^7.0.3", | ||
63 | "eslint": "^8.21.0", | ||
64 | "eslint-config-airbnb": "^19.0.4", | ||
65 | "eslint-config-airbnb-typescript": "^17.0.0", | ||
66 | "eslint-config-prettier": "^8.5.0", | ||
67 | "eslint-import-resolver-typescript": "^3.4.0", | ||
68 | "eslint-plugin-import": "^2.26.0", | ||
69 | "eslint-plugin-jsx-a11y": "^6.6.1", | ||
70 | "eslint-plugin-prettier": "^4.2.1", | ||
71 | "eslint-plugin-react": "^7.30.1", | ||
72 | "eslint-plugin-react-hooks": "^4.6.0", | ||
73 | "prettier": "^2.7.1", | ||
74 | "rollup": "^2.77.3", | ||
75 | "typescript": "~4.7.4", | ||
76 | "vite": "^3.0.6", | ||
77 | "vite-plugin-inject-preload": "^1.0.1" | ||
93 | } | 78 | } |
94 | } | 79 | } |
diff --git a/subprojects/frontend/prettier.config.cjs b/subprojects/frontend/prettier.config.cjs new file mode 100644 index 00000000..75f5c54d --- /dev/null +++ b/subprojects/frontend/prettier.config.cjs | |||
@@ -0,0 +1,5 @@ | |||
1 | /** @type {import('prettier').Config} */ | ||
2 | module.exports = { | ||
3 | singleQuote: true, | ||
4 | trailingComma: 'all', | ||
5 | }; | ||
diff --git a/subprojects/frontend/src/App.tsx b/subprojects/frontend/src/App.tsx index 54f92f9a..d3ec63eb 100644 --- a/subprojects/frontend/src/App.tsx +++ b/subprojects/frontend/src/App.tsx | |||
@@ -1,26 +1,19 @@ | |||
1 | import MenuIcon from '@mui/icons-material/Menu'; | ||
1 | import AppBar from '@mui/material/AppBar'; | 2 | import AppBar from '@mui/material/AppBar'; |
2 | import Box from '@mui/material/Box'; | 3 | import Box from '@mui/material/Box'; |
3 | import IconButton from '@mui/material/IconButton'; | 4 | import IconButton from '@mui/material/IconButton'; |
4 | import Toolbar from '@mui/material/Toolbar'; | 5 | import Toolbar from '@mui/material/Toolbar'; |
5 | import Typography from '@mui/material/Typography'; | 6 | import Typography from '@mui/material/Typography'; |
6 | import MenuIcon from '@mui/icons-material/Menu'; | ||
7 | import React from 'react'; | 7 | import React from 'react'; |
8 | 8 | ||
9 | import { EditorArea } from './editor/EditorArea'; | 9 | import EditorArea from './editor/EditorArea'; |
10 | import { EditorButtons } from './editor/EditorButtons'; | 10 | import EditorButtons from './editor/EditorButtons'; |
11 | import { GenerateButton } from './editor/GenerateButton'; | 11 | import GenerateButton from './editor/GenerateButton'; |
12 | 12 | ||
13 | export function App(): JSX.Element { | 13 | export default function App(): JSX.Element { |
14 | return ( | 14 | return ( |
15 | <Box | 15 | <Box display="flex" flexDirection="column" sx={{ height: '100vh' }}> |
16 | display="flex" | 16 | <AppBar position="static" color="inherit"> |
17 | flexDirection="column" | ||
18 | sx={{ height: '100vh' }} | ||
19 | > | ||
20 | <AppBar | ||
21 | position="static" | ||
22 | color="inherit" | ||
23 | > | ||
24 | <Toolbar> | 17 | <Toolbar> |
25 | <IconButton | 18 | <IconButton |
26 | edge="start" | 19 | edge="start" |
@@ -30,11 +23,7 @@ export function App(): JSX.Element { | |||
30 | > | 23 | > |
31 | <MenuIcon /> | 24 | <MenuIcon /> |
32 | </IconButton> | 25 | </IconButton> |
33 | <Typography | 26 | <Typography variant="h6" component="h1" flexGrow={1}> |
34 | variant="h6" | ||
35 | component="h1" | ||
36 | flexGrow={1} | ||
37 | > | ||
38 | Refinery | 27 | Refinery |
39 | </Typography> | 28 | </Typography> |
40 | </Toolbar> | 29 | </Toolbar> |
@@ -48,11 +37,7 @@ export function App(): JSX.Element { | |||
48 | <EditorButtons /> | 37 | <EditorButtons /> |
49 | <GenerateButton /> | 38 | <GenerateButton /> |
50 | </Box> | 39 | </Box> |
51 | <Box | 40 | <Box flexGrow={1} flexShrink={1} sx={{ overflow: 'auto' }}> |
52 | flexGrow={1} | ||
53 | flexShrink={1} | ||
54 | sx={{ overflow: 'auto' }} | ||
55 | > | ||
56 | <EditorArea /> | 41 | <EditorArea /> |
57 | </Box> | 42 | </Box> |
58 | </Box> | 43 | </Box> |
diff --git a/subprojects/frontend/src/Loading.tsx b/subprojects/frontend/src/Loading.tsx new file mode 100644 index 00000000..a699adca --- /dev/null +++ b/subprojects/frontend/src/Loading.tsx | |||
@@ -0,0 +1,19 @@ | |||
1 | import CircularProgress from '@mui/material/CircularProgress'; | ||
2 | import { styled } from '@mui/material/styles'; | ||
3 | import React from 'react'; | ||
4 | |||
5 | const LoadingRoot = styled('div')({ | ||
6 | width: '100vw', | ||
7 | height: '100vh', | ||
8 | display: 'flex', | ||
9 | alignItems: 'center', | ||
10 | justifyContent: 'center', | ||
11 | }); | ||
12 | |||
13 | export default function Loading() { | ||
14 | return ( | ||
15 | <LoadingRoot> | ||
16 | <CircularProgress /> | ||
17 | </LoadingRoot> | ||
18 | ); | ||
19 | } | ||
diff --git a/subprojects/frontend/src/RootStore.tsx b/subprojects/frontend/src/RootStore.tsx index baf0b61e..a7406d7b 100644 --- a/subprojects/frontend/src/RootStore.tsx +++ b/subprojects/frontend/src/RootStore.tsx | |||
@@ -1,9 +1,9 @@ | |||
1 | import React, { createContext, useContext } from 'react'; | 1 | import React, { createContext, useContext } from 'react'; |
2 | 2 | ||
3 | import { EditorStore } from './editor/EditorStore'; | 3 | import EditorStore from './editor/EditorStore'; |
4 | import { ThemeStore } from './theme/ThemeStore'; | 4 | import ThemeStore from './theme/ThemeStore'; |
5 | 5 | ||
6 | export class RootStore { | 6 | export default class RootStore { |
7 | editorStore; | 7 | editorStore; |
8 | 8 | ||
9 | themeStore; | 9 | themeStore; |
@@ -22,11 +22,12 @@ export interface RootStoreProviderProps { | |||
22 | rootStore: RootStore; | 22 | rootStore: RootStore; |
23 | } | 23 | } |
24 | 24 | ||
25 | export function RootStoreProvider({ children, rootStore }: RootStoreProviderProps): JSX.Element { | 25 | export function RootStoreProvider({ |
26 | children, | ||
27 | rootStore, | ||
28 | }: RootStoreProviderProps): JSX.Element { | ||
26 | return ( | 29 | return ( |
27 | <StoreContext.Provider value={rootStore}> | 30 | <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider> |
28 | {children} | ||
29 | </StoreContext.Provider> | ||
30 | ); | 31 | ); |
31 | } | 32 | } |
32 | 33 | ||
diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx index dba20f6e..14294371 100644 --- a/subprojects/frontend/src/editor/EditorArea.tsx +++ b/subprojects/frontend/src/editor/EditorArea.tsx | |||
@@ -1,17 +1,13 @@ | |||
1 | import { Command, EditorView } from '@codemirror/view'; | ||
2 | import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; | ||
3 | import { closeLintPanel, openLintPanel } from '@codemirror/lint'; | 1 | import { closeLintPanel, openLintPanel } from '@codemirror/lint'; |
2 | import { closeSearchPanel, openSearchPanel } from '@codemirror/search'; | ||
3 | import { type Command, EditorView } from '@codemirror/view'; | ||
4 | import { observer } from 'mobx-react-lite'; | 4 | import { observer } from 'mobx-react-lite'; |
5 | import React, { | 5 | import React, { useCallback, useEffect, useRef, useState } from 'react'; |
6 | useCallback, | ||
7 | useEffect, | ||
8 | useRef, | ||
9 | useState, | ||
10 | } from 'react'; | ||
11 | 6 | ||
12 | import { EditorParent } from './EditorParent'; | ||
13 | import { useRootStore } from '../RootStore'; | 7 | import { useRootStore } from '../RootStore'; |
14 | import { getLogger } from '../utils/logger'; | 8 | import getLogger from '../utils/getLogger'; |
9 | |||
10 | import EditorParent from './EditorParent'; | ||
15 | 11 | ||
16 | const log = getLogger('editor.EditorArea'); | 12 | const log = getLogger('editor.EditorArea'); |
17 | 13 | ||
@@ -70,10 +66,12 @@ function fixCodeMirrorAccessibility(editorView: EditorView) { | |||
70 | contentDOM.setAttribute('aria-label', 'Code editor'); | 66 | contentDOM.setAttribute('aria-label', 'Code editor'); |
71 | } | 67 | } |
72 | 68 | ||
73 | export const EditorArea = observer(() => { | 69 | function EditorArea(): JSX.Element { |
74 | const { editorStore } = useRootStore(); | 70 | const { editorStore } = useRootStore(); |
75 | const editorParentRef = useRef<HTMLDivElement | null>(null); | 71 | const editorParentRef = useRef<HTMLDivElement | null>(null); |
76 | const [editorViewState, setEditorViewState] = useState<EditorView | null>(null); | 72 | const [editorViewState, setEditorViewState] = useState<EditorView | null>( |
73 | null, | ||
74 | ); | ||
77 | 75 | ||
78 | const setSearchPanelOpen = usePanel( | 76 | const setSearchPanelOpen = usePanel( |
79 | 'search', | 77 | 'search', |
@@ -131,22 +129,21 @@ export const EditorArea = observer(() => { | |||
131 | editorView.destroy(); | 129 | editorView.destroy(); |
132 | log.info('Editor destroyed'); | 130 | log.info('Editor destroyed'); |
133 | }; | 131 | }; |
134 | }, [ | 132 | }, [editorStore, setSearchPanelOpen, setLintPanelOpen]); |
135 | editorParentRef, | ||
136 | editorStore, | ||
137 | setSearchPanelOpen, | ||
138 | setLintPanelOpen, | ||
139 | ]); | ||
140 | 133 | ||
141 | return ( | 134 | return ( |
142 | <EditorParent | 135 | <EditorParent |
143 | className="dark" | 136 | className="dark" |
144 | sx={{ | 137 | sx={{ |
145 | '.cm-lineNumbers': editorStore.showLineNumbers ? {} : { | 138 | '.cm-lineNumbers': editorStore.showLineNumbers |
146 | display: 'none !important', | 139 | ? {} |
147 | }, | 140 | : { |
141 | display: 'none !important', | ||
142 | }, | ||
148 | }} | 143 | }} |
149 | ref={editorParentRef} | 144 | ref={editorParentRef} |
150 | /> | 145 | /> |
151 | ); | 146 | ); |
152 | }); | 147 | } |
148 | |||
149 | export default observer(EditorArea); | ||
diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx index 150aa00d..652ca71e 100644 --- a/subprojects/frontend/src/editor/EditorButtons.tsx +++ b/subprojects/frontend/src/editor/EditorButtons.tsx | |||
@@ -1,9 +1,4 @@ | |||
1 | import type { Diagnostic } from '@codemirror/lint'; | 1 | import type { Diagnostic } from '@codemirror/lint'; |
2 | import { observer } from 'mobx-react-lite'; | ||
3 | import IconButton from '@mui/material/IconButton'; | ||
4 | import Stack from '@mui/material/Stack'; | ||
5 | import ToggleButton from '@mui/material/ToggleButton'; | ||
6 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | ||
7 | import CheckIcon from '@mui/icons-material/Check'; | 2 | import CheckIcon from '@mui/icons-material/Check'; |
8 | import ErrorIcon from '@mui/icons-material/Error'; | 3 | import ErrorIcon from '@mui/icons-material/Error'; |
9 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 4 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
@@ -13,6 +8,11 @@ import RedoIcon from '@mui/icons-material/Redo'; | |||
13 | import SearchIcon from '@mui/icons-material/Search'; | 8 | import SearchIcon from '@mui/icons-material/Search'; |
14 | import UndoIcon from '@mui/icons-material/Undo'; | 9 | import UndoIcon from '@mui/icons-material/Undo'; |
15 | import WarningIcon from '@mui/icons-material/Warning'; | 10 | import WarningIcon from '@mui/icons-material/Warning'; |
11 | import IconButton from '@mui/material/IconButton'; | ||
12 | import Stack from '@mui/material/Stack'; | ||
13 | import ToggleButton from '@mui/material/ToggleButton'; | ||
14 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; | ||
15 | import { observer } from 'mobx-react-lite'; | ||
16 | import React from 'react'; | 16 | import React from 'react'; |
17 | 17 | ||
18 | import { useRootStore } from '../RootStore'; | 18 | import { useRootStore } from '../RootStore'; |
@@ -27,23 +27,17 @@ function getLintIcon(severity: Diagnostic['severity'] | null) { | |||
27 | return <WarningIcon fontSize="small" />; | 27 | return <WarningIcon fontSize="small" />; |
28 | case 'info': | 28 | case 'info': |
29 | return <InfoOutlinedIcon fontSize="small" />; | 29 | return <InfoOutlinedIcon fontSize="small" />; |
30 | case null: | 30 | default: |
31 | return <CheckIcon fontSize="small" />; | 31 | return <CheckIcon fontSize="small" />; |
32 | } | 32 | } |
33 | } | 33 | } |
34 | 34 | ||
35 | export const EditorButtons = observer(() => { | 35 | function EditorButtons(): JSX.Element { |
36 | const { editorStore } = useRootStore(); | 36 | const { editorStore } = useRootStore(); |
37 | 37 | ||
38 | return ( | 38 | return ( |
39 | <Stack | 39 | <Stack direction="row" spacing={1}> |
40 | direction="row" | 40 | <Stack direction="row" alignItems="center"> |
41 | spacing={1} | ||
42 | > | ||
43 | <Stack | ||
44 | direction="row" | ||
45 | alignItems="center" | ||
46 | > | ||
47 | <IconButton | 41 | <IconButton |
48 | disabled={!editorStore.canUndo} | 42 | disabled={!editorStore.canUndo} |
49 | onClick={() => editorStore.undo()} | 43 | onClick={() => editorStore.undo()} |
@@ -59,9 +53,7 @@ export const EditorButtons = observer(() => { | |||
59 | <RedoIcon fontSize="small" /> | 53 | <RedoIcon fontSize="small" /> |
60 | </IconButton> | 54 | </IconButton> |
61 | </Stack> | 55 | </Stack> |
62 | <ToggleButtonGroup | 56 | <ToggleButtonGroup size="small"> |
63 | size="small" | ||
64 | > | ||
65 | <ToggleButton | 57 | <ToggleButton |
66 | selected={editorStore.showLineNumbers} | 58 | selected={editorStore.showLineNumbers} |
67 | onClick={() => editorStore.toggleLineNumbers()} | 59 | onClick={() => editorStore.toggleLineNumbers()} |
@@ -95,4 +87,6 @@ export const EditorButtons = observer(() => { | |||
95 | </IconButton> | 87 | </IconButton> |
96 | </Stack> | 88 | </Stack> |
97 | ); | 89 | ); |
98 | }); | 90 | } |
91 | |||
92 | export default observer(EditorButtons); | ||
diff --git a/subprojects/frontend/src/editor/EditorParent.ts b/subprojects/frontend/src/editor/EditorParent.ts index 9aaf541a..dbc35a0d 100644 --- a/subprojects/frontend/src/editor/EditorParent.ts +++ b/subprojects/frontend/src/editor/EditorParent.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { styled } from '@mui/material/styles'; | 1 | import { alpha, styled } from '@mui/material/styles'; |
2 | 2 | ||
3 | /** | 3 | /** |
4 | * Returns a squiggly underline background image encoded as a CSS `url()` data URI with Base64. | 4 | * Returns a squiggly underline background image encoded as a CSS `url()` data URI with Base64. |
@@ -17,7 +17,9 @@ function underline(color: string) { | |||
17 | return `url('data:image/svg+xml;base64,${svgBase64}')`; | 17 | return `url('data:image/svg+xml;base64,${svgBase64}')`; |
18 | } | 18 | } |
19 | 19 | ||
20 | export const EditorParent = styled('div')(({ theme }) => { | 20 | export default styled('div', { |
21 | name: 'EditorParent', | ||
22 | })(({ theme }) => { | ||
21 | const codeMirrorLintStyle: Record<string, unknown> = {}; | 23 | const codeMirrorLintStyle: Record<string, unknown> = {}; |
22 | (['error', 'warning', 'info'] as const).forEach((severity) => { | 24 | (['error', 'warning', 'info'] as const).forEach((severity) => { |
23 | const color = theme.palette[severity].main; | 25 | const color = theme.palette[severity].main; |
@@ -37,19 +39,20 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
37 | '.cm-content': { | 39 | '.cm-content': { |
38 | padding: 0, | 40 | padding: 0, |
39 | }, | 41 | }, |
40 | '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { | 42 | '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': |
41 | fontSize: 16, | 43 | { |
42 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', | 44 | fontSize: 16, |
43 | fontFeatureSettings: '"liga", "calt"', | 45 | fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', |
44 | fontWeight: 400, | 46 | fontFeatureSettings: '"liga", "calt"', |
45 | letterSpacing: 0, | 47 | fontWeight: 400, |
46 | textRendering: 'optimizeLegibility', | 48 | letterSpacing: 0, |
47 | }, | 49 | textRendering: 'optimizeLegibility', |
50 | }, | ||
48 | '.cm-scroller': { | 51 | '.cm-scroller': { |
49 | color: theme.palette.text.secondary, | 52 | color: theme.palette.text.secondary, |
50 | }, | 53 | }, |
51 | '.cm-gutters': { | 54 | '.cm-gutters': { |
52 | background: 'rgba(255, 255, 255, 0.1)', | 55 | background: 'transparent', |
53 | color: theme.palette.text.disabled, | 56 | color: theme.palette.text.disabled, |
54 | border: 'none', | 57 | border: 'none', |
55 | }, | 58 | }, |
@@ -57,7 +60,19 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
57 | color: theme.palette.secondary.main, | 60 | color: theme.palette.secondary.main, |
58 | }, | 61 | }, |
59 | '.cm-activeLine': { | 62 | '.cm-activeLine': { |
60 | background: 'rgba(0, 0, 0, 0.3)', | 63 | background: alpha(theme.palette.text.secondary, 0.06), |
64 | }, | ||
65 | '.cm-foldGutter': { | ||
66 | color: alpha(theme.palette.text.primary, 0), | ||
67 | transition: theme.transitions.create('color', { | ||
68 | duration: theme.transitions.duration.short, | ||
69 | }), | ||
70 | '@media (hover: none)': { | ||
71 | color: theme.palette.text.primary, | ||
72 | }, | ||
73 | }, | ||
74 | '.cm-gutters:hover .cm-foldGutter': { | ||
75 | color: theme.palette.text.primary, | ||
61 | }, | 76 | }, |
62 | '.cm-activeLineGutter': { | 77 | '.cm-activeLineGutter': { |
63 | background: 'transparent', | 78 | background: 'transparent', |
@@ -66,8 +81,7 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
66 | color: theme.palette.text.primary, | 81 | color: theme.palette.text.primary, |
67 | }, | 82 | }, |
68 | '.cm-cursor, .cm-cursor-primary': { | 83 | '.cm-cursor, .cm-cursor-primary': { |
69 | borderColor: theme.palette.primary.main, | 84 | borderLeft: `2px solid ${theme.palette.primary.main}`, |
70 | background: theme.palette.common.black, | ||
71 | }, | 85 | }, |
72 | '.cm-selectionBackground': { | 86 | '.cm-selectionBackground': { |
73 | background: '#3e4453', | 87 | background: '#3e4453', |
@@ -115,9 +129,26 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
115 | }, | 129 | }, |
116 | }, | 130 | }, |
117 | '.cm-foldPlaceholder': { | 131 | '.cm-foldPlaceholder': { |
118 | background: theme.palette.background.paper, | ||
119 | borderColor: theme.palette.text.disabled, | ||
120 | color: theme.palette.text.secondary, | 132 | color: theme.palette.text.secondary, |
133 | backgroundColor: alpha(theme.palette.text.secondary, 0), | ||
134 | border: `1px solid ${alpha(theme.palette.text.secondary, 0.5)}`, | ||
135 | borderRadius: theme.shape.borderRadius, | ||
136 | transition: theme.transitions.create( | ||
137 | ['background-color', 'border-color', 'color'], | ||
138 | { | ||
139 | duration: theme.transitions.duration.short, | ||
140 | }, | ||
141 | ), | ||
142 | '&:hover': { | ||
143 | backgroundColor: alpha( | ||
144 | theme.palette.text.secondary, | ||
145 | theme.palette.action.hoverOpacity, | ||
146 | ), | ||
147 | borderColor: theme.palette.text.secondary, | ||
148 | '@media (hover: none)': { | ||
149 | backgroundColor: 'transparent', | ||
150 | }, | ||
151 | }, | ||
121 | }, | 152 | }, |
122 | '.tok-comment': { | 153 | '.tok-comment': { |
123 | fontStyle: 'italic', | 154 | fontStyle: 'italic', |
@@ -168,9 +199,14 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
168 | }, | 199 | }, |
169 | '.cm-tooltip-autocomplete': { | 200 | '.cm-tooltip-autocomplete': { |
170 | background: theme.palette.background.paper, | 201 | background: theme.palette.background.paper, |
171 | boxShadow: `0px 2px 4px -1px rgb(0 0 0 / 20%), | 202 | ...(theme.palette.mode === 'dark' && { |
172 | 0px 4px 5px 0px rgb(0 0 0 / 14%), | 203 | overflow: 'hidden', |
173 | 0px 1px 10px 0px rgb(0 0 0 / 12%)`, | 204 | borderRadius: theme.shape.borderRadius, |
205 | // https://github.com/mui/material-ui/blob/10c72729c7d03bab8cdce6eb422642684c56dca2/packages/mui-material/src/Paper/Paper.js#L18 | ||
206 | backgroundImage: | ||
207 | 'linear-gradient(rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09))', | ||
208 | }), | ||
209 | boxShadow: theme.shadows[4], | ||
174 | '.cm-completionIcon': { | 210 | '.cm-completionIcon': { |
175 | color: theme.palette.text.secondary, | 211 | color: theme.palette.text.secondary, |
176 | }, | 212 | }, |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 0f4d2936..f75147a4 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -21,18 +21,14 @@ import { | |||
21 | indentOnInput, | 21 | indentOnInput, |
22 | syntaxHighlighting, | 22 | syntaxHighlighting, |
23 | } from '@codemirror/language'; | 23 | } from '@codemirror/language'; |
24 | import { | 24 | import { type Diagnostic, lintKeymap, setDiagnostics } from '@codemirror/lint'; |
25 | Diagnostic, | ||
26 | lintKeymap, | ||
27 | setDiagnostics, | ||
28 | } from '@codemirror/lint'; | ||
29 | import { search, searchKeymap } from '@codemirror/search'; | 25 | import { search, searchKeymap } from '@codemirror/search'; |
30 | import { | 26 | import { |
31 | EditorState, | 27 | EditorState, |
32 | StateCommand, | 28 | type StateCommand, |
33 | StateEffect, | 29 | StateEffect, |
34 | Transaction, | 30 | type Transaction, |
35 | TransactionSpec, | 31 | type TransactionSpec, |
36 | } from '@codemirror/state'; | 32 | } from '@codemirror/state'; |
37 | import { | 33 | import { |
38 | drawSelection, | 34 | drawSelection, |
@@ -45,26 +41,25 @@ import { | |||
45 | rectangularSelection, | 41 | rectangularSelection, |
46 | } from '@codemirror/view'; | 42 | } from '@codemirror/view'; |
47 | import { classHighlighter } from '@lezer/highlight'; | 43 | import { classHighlighter } from '@lezer/highlight'; |
48 | import { | 44 | import { makeAutoObservable, observable, reaction } from 'mobx'; |
49 | makeAutoObservable, | 45 | |
50 | observable, | 46 | import problemLanguageSupport from '../language/problemLanguageSupport'; |
51 | reaction, | 47 | import type ThemeStore from '../theme/ThemeStore'; |
52 | } from 'mobx'; | 48 | import getLogger from '../utils/getLogger'; |
53 | 49 | import XtextClient from '../xtext/XtextClient'; | |
54 | import { findOccurrences, IOccurrence, setOccurrences } from './findOccurrences'; | 50 | |
55 | import { problemLanguageSupport } from '../language/problemLanguageSupport'; | 51 | import findOccurrences, { |
56 | import { | 52 | type IOccurrence, |
57 | IHighlightRange, | 53 | setOccurrences, |
58 | semanticHighlighting, | 54 | } from './findOccurrences'; |
55 | import semanticHighlighting, { | ||
56 | type IHighlightRange, | ||
59 | setSemanticHighlighting, | 57 | setSemanticHighlighting, |
60 | } from './semanticHighlighting'; | 58 | } from './semanticHighlighting'; |
61 | import type { ThemeStore } from '../theme/ThemeStore'; | ||
62 | import { getLogger } from '../utils/logger'; | ||
63 | import { XtextClient } from '../xtext/XtextClient'; | ||
64 | 59 | ||
65 | const log = getLogger('editor.EditorStore'); | 60 | const log = getLogger('editor.EditorStore'); |
66 | 61 | ||
67 | export class EditorStore { | 62 | export default class EditorStore { |
68 | private readonly themeStore; | 63 | private readonly themeStore; |
69 | 64 | ||
70 | state: EditorState; | 65 | state: EditorState; |
@@ -96,17 +91,18 @@ export class EditorStore { | |||
96 | extensions: [ | 91 | extensions: [ |
97 | autocompletion({ | 92 | autocompletion({ |
98 | activateOnTyping: true, | 93 | activateOnTyping: true, |
99 | override: [ | 94 | override: [(context) => this.client.contentAssist(context)], |
100 | (context) => this.client.contentAssist(context), | ||
101 | ], | ||
102 | }), | 95 | }), |
103 | closeBrackets(), | 96 | closeBrackets(), |
104 | bracketMatching(), | 97 | bracketMatching(), |
105 | drawSelection(), | 98 | drawSelection(), |
106 | EditorState.allowMultipleSelections.of(true), | 99 | EditorState.allowMultipleSelections.of(true), |
107 | EditorView.theme({}, { | 100 | EditorView.theme( |
108 | dark: this.themeStore.darkMode, | 101 | {}, |
109 | }), | 102 | { |
103 | dark: this.themeStore.darkMode, | ||
104 | }, | ||
105 | ), | ||
110 | findOccurrences, | 106 | findOccurrences, |
111 | highlightActiveLine(), | 107 | highlightActiveLine(), |
112 | highlightActiveLineGutter(), | 108 | highlightActiveLineGutter(), |
@@ -134,8 +130,16 @@ export class EditorStore { | |||
134 | { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, | 130 | { key: 'Mod-Shift-m', run: () => this.setLintPanelOpen(true) }, |
135 | ...lintKeymap, | 131 | ...lintKeymap, |
136 | // Override keys in `searchKeymap` to go through the `EditorStore`. | 132 | // Override keys in `searchKeymap` to go through the `EditorStore`. |
137 | { key: 'Mod-f', run: () => this.setSearchPanelOpen(true), scope: 'editor search-panel' }, | 133 | { |
138 | { key: 'Escape', run: () => this.setSearchPanelOpen(false), scope: 'editor search-panel' }, | 134 | key: 'Mod-f', |
135 | run: () => this.setSearchPanelOpen(true), | ||
136 | scope: 'editor search-panel', | ||
137 | }, | ||
138 | { | ||
139 | key: 'Escape', | ||
140 | run: () => this.setSearchPanelOpen(false), | ||
141 | scope: 'editor search-panel', | ||
142 | }, | ||
139 | ...searchKeymap, | 143 | ...searchKeymap, |
140 | ...defaultKeymap, | 144 | ...defaultKeymap, |
141 | ]), | 145 | ]), |
@@ -149,9 +153,14 @@ export class EditorStore { | |||
149 | log.debug('Update editor dark mode', darkMode); | 153 | log.debug('Update editor dark mode', darkMode); |
150 | this.dispatch({ | 154 | this.dispatch({ |
151 | effects: [ | 155 | effects: [ |
152 | StateEffect.appendConfig.of(EditorView.theme({}, { | 156 | StateEffect.appendConfig.of( |
153 | dark: darkMode, | 157 | EditorView.theme( |
154 | })), | 158 | {}, |
159 | { | ||
160 | dark: darkMode, | ||
161 | }, | ||
162 | ), | ||
163 | ), | ||
155 | ], | 164 | ], |
156 | }); | 165 | }); |
157 | }, | 166 | }, |
@@ -198,6 +207,8 @@ export class EditorStore { | |||
198 | case 'info': | 207 | case 'info': |
199 | this.infoCount += 1; | 208 | this.infoCount += 1; |
200 | break; | 209 | break; |
210 | default: | ||
211 | throw new Error('Unknown severity'); | ||
201 | } | 212 | } |
202 | }); | 213 | }); |
203 | } | 214 | } |
@@ -261,7 +272,7 @@ export class EditorStore { | |||
261 | * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` | 272 | * This matches the behavior of the `openSearchPanel` and `closeSearchPanel` |
262 | * commands from `'@codemirror/search'`. | 273 | * commands from `'@codemirror/search'`. |
263 | * | 274 | * |
264 | * @param newShosSearchPanel whether we should show the search panel | 275 | * @param newShowSearchPanel whether we should show the search panel |
265 | * @returns `true` if the state was changed, `false` otherwise | 276 | * @returns `true` if the state was changed, `false` otherwise |
266 | */ | 277 | */ |
267 | setSearchPanelOpen(newShowSearchPanel: boolean): boolean { | 278 | setSearchPanelOpen(newShowSearchPanel: boolean): boolean { |
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 3834cec4..fc337da9 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx | |||
@@ -1,13 +1,13 @@ | |||
1 | import { observer } from 'mobx-react-lite'; | ||
2 | import Button from '@mui/material/Button'; | ||
3 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | 1 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; |
2 | import Button from '@mui/material/Button'; | ||
3 | import { observer } from 'mobx-react-lite'; | ||
4 | import React from 'react'; | 4 | import React from 'react'; |
5 | 5 | ||
6 | import { useRootStore } from '../RootStore'; | 6 | import { useRootStore } from '../RootStore'; |
7 | 7 | ||
8 | const GENERATE_LABEL = 'Generate'; | 8 | const GENERATE_LABEL = 'Generate'; |
9 | 9 | ||
10 | export const GenerateButton = observer(() => { | 10 | function GenerateButton(): JSX.Element { |
11 | const { editorStore } = useRootStore(); | 11 | const { editorStore } = useRootStore(); |
12 | const { errorCount, warningCount } = editorStore; | 12 | const { errorCount, warningCount } = editorStore; |
13 | 13 | ||
@@ -41,4 +41,6 @@ export const GenerateButton = observer(() => { | |||
41 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} | 41 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} |
42 | </Button> | 42 | </Button> |
43 | ); | 43 | ); |
44 | }); | 44 | } |
45 | |||
46 | export default observer(GenerateButton); | ||
diff --git a/subprojects/frontend/src/editor/decorationSetExtension.ts b/subprojects/frontend/src/editor/defineDecorationSetExtension.ts index 2d630c20..d9c7bc7d 100644 --- a/subprojects/frontend/src/editor/decorationSetExtension.ts +++ b/subprojects/frontend/src/editor/defineDecorationSetExtension.ts | |||
@@ -1,11 +1,16 @@ | |||
1 | import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; | 1 | import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; |
2 | import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; | 2 | import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; |
3 | 3 | ||
4 | export type TransactionSpecFactory = (decorations: DecorationSet) => TransactionSpec; | 4 | export type TransactionSpecFactory = ( |
5 | decorations: DecorationSet, | ||
6 | ) => TransactionSpec; | ||
5 | 7 | ||
6 | export function decorationSetExtension(): [TransactionSpecFactory, StateField<DecorationSet>] { | 8 | export default function defineDecorationSetExtension(): [ |
9 | TransactionSpecFactory, | ||
10 | StateField<DecorationSet>, | ||
11 | ] { | ||
7 | const setEffect = StateEffect.define<DecorationSet>(); | 12 | const setEffect = StateEffect.define<DecorationSet>(); |
8 | const field = StateField.define<DecorationSet>({ | 13 | const stateField = StateField.define<DecorationSet>({ |
9 | create() { | 14 | create() { |
10 | return Decoration.none; | 15 | return Decoration.none; |
11 | }, | 16 | }, |
@@ -24,16 +29,14 @@ export function decorationSetExtension(): [TransactionSpecFactory, StateField<De | |||
24 | } | 29 | } |
25 | return newDecorations; | 30 | return newDecorations; |
26 | }, | 31 | }, |
27 | provide: (f) => EditorView.decorations.from(f), | 32 | provide: (field) => EditorView.decorations.from(field), |
28 | }); | 33 | }); |
29 | 34 | ||
30 | function transactionSpecFactory(decorations: DecorationSet) { | 35 | function transactionSpecFactory(decorations: DecorationSet) { |
31 | return { | 36 | return { |
32 | effects: [ | 37 | effects: [setEffect.of(decorations)], |
33 | setEffect.of(decorations), | ||
34 | ], | ||
35 | }; | 38 | }; |
36 | } | 39 | } |
37 | 40 | ||
38 | return [transactionSpecFactory, field]; | 41 | return [transactionSpecFactory, stateField]; |
39 | } | 42 | } |
diff --git a/subprojects/frontend/src/editor/findOccurrences.ts b/subprojects/frontend/src/editor/findOccurrences.ts index c4a4e8ec..d7aae8d1 100644 --- a/subprojects/frontend/src/editor/findOccurrences.ts +++ b/subprojects/frontend/src/editor/findOccurrences.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Range, RangeSet, type TransactionSpec } from '@codemirror/state'; | 1 | import { type Range, RangeSet, type TransactionSpec } from '@codemirror/state'; |
2 | import { Decoration } from '@codemirror/view'; | 2 | import { Decoration } from '@codemirror/view'; |
3 | 3 | ||
4 | import { decorationSetExtension } from './decorationSetExtension'; | 4 | import defineDecorationSetExtension from './defineDecorationSetExtension'; |
5 | 5 | ||
6 | export interface IOccurrence { | 6 | export interface IOccurrence { |
7 | from: number; | 7 | from: number; |
@@ -9,7 +9,7 @@ export interface IOccurrence { | |||
9 | to: number; | 9 | to: number; |
10 | } | 10 | } |
11 | 11 | ||
12 | const [setOccurrencesInteral, findOccurrences] = decorationSetExtension(); | 12 | const [setOccurrencesInteral, findOccurrences] = defineDecorationSetExtension(); |
13 | 13 | ||
14 | const writeDecoration = Decoration.mark({ | 14 | const writeDecoration = Decoration.mark({ |
15 | class: 'cm-problem-write', | 15 | class: 'cm-problem-write', |
@@ -19,7 +19,10 @@ const readDecoration = Decoration.mark({ | |||
19 | class: 'cm-problem-read', | 19 | class: 'cm-problem-read', |
20 | }); | 20 | }); |
21 | 21 | ||
22 | export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): TransactionSpec { | 22 | export function setOccurrences( |
23 | write: IOccurrence[], | ||
24 | read: IOccurrence[], | ||
25 | ): TransactionSpec { | ||
23 | const decorations: Range<Decoration>[] = []; | 26 | const decorations: Range<Decoration>[] = []; |
24 | write.forEach(({ from, to }) => { | 27 | write.forEach(({ from, to }) => { |
25 | decorations.push(writeDecoration.range(from, to)); | 28 | decorations.push(writeDecoration.range(from, to)); |
@@ -31,4 +34,4 @@ export function setOccurrences(write: IOccurrence[], read: IOccurrence[]): Trans | |||
31 | return setOccurrencesInteral(rangeSet); | 34 | return setOccurrencesInteral(rangeSet); |
32 | } | 35 | } |
33 | 36 | ||
34 | export { findOccurrences }; | 37 | export default findOccurrences; |
diff --git a/subprojects/frontend/src/editor/semanticHighlighting.ts b/subprojects/frontend/src/editor/semanticHighlighting.ts index a5d0af7a..2c1bd67d 100644 --- a/subprojects/frontend/src/editor/semanticHighlighting.ts +++ b/subprojects/frontend/src/editor/semanticHighlighting.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { RangeSet, type TransactionSpec } from '@codemirror/state'; | 1 | import { RangeSet, type TransactionSpec } from '@codemirror/state'; |
2 | import { Decoration } from '@codemirror/view'; | 2 | import { Decoration } from '@codemirror/view'; |
3 | 3 | ||
4 | import { decorationSetExtension } from './decorationSetExtension'; | 4 | import defineDecorationSetExtension from './defineDecorationSetExtension'; |
5 | 5 | ||
6 | export interface IHighlightRange { | 6 | export interface IHighlightRange { |
7 | from: number; | 7 | from: number; |
@@ -11,13 +11,21 @@ export interface IHighlightRange { | |||
11 | classes: string[]; | 11 | classes: string[]; |
12 | } | 12 | } |
13 | 13 | ||
14 | const [setSemanticHighlightingInternal, semanticHighlighting] = decorationSetExtension(); | 14 | const [setSemanticHighlightingInternal, semanticHighlighting] = |
15 | defineDecorationSetExtension(); | ||
15 | 16 | ||
16 | export function setSemanticHighlighting(ranges: IHighlightRange[]): TransactionSpec { | 17 | export function setSemanticHighlighting( |
17 | const rangeSet = RangeSet.of(ranges.map(({ from, to, classes }) => Decoration.mark({ | 18 | ranges: IHighlightRange[], |
18 | class: classes.map((c) => `tok-problem-${c}`).join(' '), | 19 | ): TransactionSpec { |
19 | }).range(from, to)), true); | 20 | const rangeSet = RangeSet.of( |
21 | ranges.map(({ from, to, classes }) => | ||
22 | Decoration.mark({ | ||
23 | class: classes.map((c) => `tok-problem-${c}`).join(' '), | ||
24 | }).range(from, to), | ||
25 | ), | ||
26 | true, | ||
27 | ); | ||
20 | return setSemanticHighlightingInternal(rangeSet); | 28 | return setSemanticHighlightingInternal(rangeSet); |
21 | } | 29 | } |
22 | 30 | ||
23 | export { semanticHighlighting }; | 31 | export default semanticHighlighting; |
diff --git a/subprojects/frontend/src/global.d.ts b/subprojects/frontend/src/global.d.ts deleted file mode 100644 index 0533a46e..00000000 --- a/subprojects/frontend/src/global.d.ts +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | declare const DEBUG: boolean; | ||
2 | |||
3 | declare const PACKAGE_NAME: string; | ||
4 | |||
5 | declare const PACKAGE_VERSION: string; | ||
6 | |||
7 | declare module '*.module.scss' { | ||
8 | const cssVariables: { [key in string]?: string }; | ||
9 | // eslint-disable-next-line import/no-default-export | ||
10 | export default cssVariables; | ||
11 | } | ||
diff --git a/subprojects/frontend/src/index.scss b/subprojects/frontend/src/index.scss deleted file mode 100644 index ad876aaf..00000000 --- a/subprojects/frontend/src/index.scss +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | @use '@fontsource/roboto/scss/mixins' as Roboto; | ||
2 | @use '@fontsource/jetbrains-mono/scss/mixins' as JetbrainsMono; | ||
3 | |||
4 | $fontWeights: 300, 400, 500, 700; | ||
5 | @each $weight in $fontWeights { | ||
6 | @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight); | ||
7 | @include Roboto.fontFace($fontName: 'Roboto', $weight: $weight, $style: italic); | ||
8 | } | ||
9 | |||
10 | $monoFontWeights: 400, 700; | ||
11 | @each $weight in $monoFontWeights { | ||
12 | @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight); | ||
13 | @include JetbrainsMono.fontFace($fontName: 'JetBrains Mono', $weight: $weight, $style: italic); | ||
14 | } | ||
15 | @include JetbrainsMono.fontFaceVariable($fontName: 'JetBrains MonoVariable'); | ||
16 | @include JetbrainsMono.fontFaceVariable($fontName: 'JetBrains MonoVariable', $style: italic); | ||
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx index 152c0bf7..2176b277 100644 --- a/subprojects/frontend/src/index.tsx +++ b/subprojects/frontend/src/index.tsx | |||
@@ -1,13 +1,25 @@ | |||
1 | import React from 'react'; | ||
2 | import { createRoot } from 'react-dom/client'; | ||
3 | import CssBaseline from '@mui/material/CssBaseline'; | 1 | import CssBaseline from '@mui/material/CssBaseline'; |
2 | import React, { Suspense, lazy } from 'react'; | ||
3 | import { createRoot } from 'react-dom/client'; | ||
4 | import '@fontsource/jetbrains-mono/400.css'; | ||
5 | import '@fontsource/jetbrains-mono/400-italic.css'; | ||
6 | import '@fontsource/jetbrains-mono/700.css'; | ||
7 | import '@fontsource/jetbrains-mono/700-italic.css'; | ||
8 | import '@fontsource/jetbrains-mono/variable.css'; | ||
9 | import '@fontsource/jetbrains-mono/variable-italic.css'; | ||
10 | import '@fontsource/roboto/300.css'; | ||
11 | import '@fontsource/roboto/300-italic.css'; | ||
12 | import '@fontsource/roboto/400.css'; | ||
13 | import '@fontsource/roboto/400-italic.css'; | ||
14 | import '@fontsource/roboto/500.css'; | ||
15 | import '@fontsource/roboto/500-italic.css'; | ||
16 | import '@fontsource/roboto/700.css'; | ||
17 | import '@fontsource/roboto/700-italic.css'; | ||
4 | 18 | ||
5 | import { App } from './App'; | 19 | import Loading from './Loading'; |
6 | import { RootStore, RootStoreProvider } from './RootStore'; | 20 | import RootStore, { RootStoreProvider } from './RootStore'; |
7 | import { ThemeProvider } from './theme/ThemeProvider'; | 21 | import ThemeProvider from './theme/ThemeProvider'; |
8 | import { getLogger } from './utils/logger'; | 22 | import getLogger from './utils/getLogger'; |
9 | |||
10 | import './index.scss'; | ||
11 | 23 | ||
12 | const log = getLogger('index'); | 24 | const log = getLogger('index'); |
13 | 25 | ||
@@ -60,13 +72,19 @@ scope Family = 1, Person += 5..10. | |||
60 | 72 | ||
61 | const rootStore = new RootStore(initialValue); | 73 | const rootStore = new RootStore(initialValue); |
62 | 74 | ||
75 | const App = lazy(() => import('./App.js')); | ||
76 | |||
63 | const app = ( | 77 | const app = ( |
64 | <RootStoreProvider rootStore={rootStore}> | 78 | <React.StrictMode> |
65 | <ThemeProvider> | 79 | <RootStoreProvider rootStore={rootStore}> |
66 | <CssBaseline /> | 80 | <ThemeProvider> |
67 | <App /> | 81 | <CssBaseline enableColorScheme /> |
68 | </ThemeProvider> | 82 | <Suspense fallback={<Loading />}> |
69 | </RootStoreProvider> | 83 | <App /> |
84 | </Suspense> | ||
85 | </ThemeProvider> | ||
86 | </RootStoreProvider> | ||
87 | </React.StrictMode> | ||
70 | ); | 88 | ); |
71 | 89 | ||
72 | const rootElement = document.getElementById('app'); | 90 | const rootElement = document.getElementById('app'); |
diff --git a/subprojects/frontend/src/language/folding.ts b/subprojects/frontend/src/language/folding.ts index 2560c183..9d1c04a3 100644 --- a/subprojects/frontend/src/language/folding.ts +++ b/subprojects/frontend/src/language/folding.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { EditorState } from '@codemirror/state'; | 1 | import type { EditorState } from '@codemirror/state'; |
2 | import type { SyntaxNode } from '@lezer/common'; | 2 | import type { SyntaxNode } from '@lezer/common'; |
3 | 3 | ||
4 | export type FoldRange = { from: number, to: number }; | 4 | export type FoldRange = { from: number; to: number }; |
5 | 5 | ||
6 | /** | 6 | /** |
7 | * Folds a block comment between its delimiters. | 7 | * Folds a block comment between its delimiters. |
@@ -47,7 +47,10 @@ export function foldBlockComment(node: SyntaxNode): FoldRange { | |||
47 | * @param state the editor state | 47 | * @param state the editor state |
48 | * @returns the folding range or `null` is there is nothing to fold | 48 | * @returns the folding range or `null` is there is nothing to fold |
49 | */ | 49 | */ |
50 | export function foldDeclaration(node: SyntaxNode, state: EditorState): FoldRange | null { | 50 | export function foldDeclaration( |
51 | node: SyntaxNode, | ||
52 | state: EditorState, | ||
53 | ): FoldRange | null { | ||
51 | const { firstChild: open, lastChild: close } = node; | 54 | const { firstChild: open, lastChild: close } = node; |
52 | if (open === null || close === null) { | 55 | if (open === null || close === null) { |
53 | return null; | 56 | return null; |
diff --git a/subprojects/frontend/src/language/indentation.ts b/subprojects/frontend/src/language/indentation.ts index 1c38637f..0bd2423c 100644 --- a/subprojects/frontend/src/language/indentation.ts +++ b/subprojects/frontend/src/language/indentation.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { TreeIndentContext } from '@codemirror/language'; | 1 | import type { TreeIndentContext } from '@codemirror/language'; |
2 | 2 | ||
3 | /** | 3 | /** |
4 | * Finds the `from` of first non-skipped token, if any, | 4 | * Finds the `from` of first non-skipped token, if any, |
@@ -11,18 +11,16 @@ import { TreeIndentContext } from '@codemirror/language'; | |||
11 | * @returns the alignment or `null` if there is no token after the opening keyword | 11 | * @returns the alignment or `null` if there is no token after the opening keyword |
12 | */ | 12 | */ |
13 | function findAlignmentAfterOpening(context: TreeIndentContext): number | null { | 13 | function findAlignmentAfterOpening(context: TreeIndentContext): number | null { |
14 | const { | 14 | const { node: tree, simulatedBreak } = context; |
15 | node: tree, | ||
16 | simulatedBreak, | ||
17 | } = context; | ||
18 | const openingToken = tree.childAfter(tree.from); | 15 | const openingToken = tree.childAfter(tree.from); |
19 | if (openingToken === null) { | 16 | if (openingToken === null) { |
20 | return null; | 17 | return null; |
21 | } | 18 | } |
22 | const openingLine = context.state.doc.lineAt(openingToken.from); | 19 | const openingLine = context.state.doc.lineAt(openingToken.from); |
23 | const lineEnd = simulatedBreak == null || simulatedBreak <= openingLine.from | 20 | const lineEnd = |
24 | ? openingLine.to | 21 | simulatedBreak == null || simulatedBreak <= openingLine.from |
25 | : Math.min(openingLine.to, simulatedBreak); | 22 | ? openingLine.to |
23 | : Math.min(openingLine.to, simulatedBreak); | ||
26 | const cursor = openingToken.cursor(); | 24 | const cursor = openingToken.cursor(); |
27 | while (cursor.next() && cursor.from < lineEnd) { | 25 | while (cursor.next() && cursor.from < lineEnd) { |
28 | if (!cursor.type.isSkipped) { | 26 | if (!cursor.type.isSkipped) { |
@@ -58,7 +56,10 @@ function findAlignmentAfterOpening(context: TreeIndentContext): number | null { | |||
58 | * @param units the number of units to indent | 56 | * @param units the number of units to indent |
59 | * @returns the desired indentation level | 57 | * @returns the desired indentation level |
60 | */ | 58 | */ |
61 | function indentDeclarationStrategy(context: TreeIndentContext, units: number): number { | 59 | function indentDeclarationStrategy( |
60 | context: TreeIndentContext, | ||
61 | units: number, | ||
62 | ): number { | ||
62 | const alignment = findAlignmentAfterOpening(context); | 63 | const alignment = findAlignmentAfterOpening(context); |
63 | if (alignment !== null) { | 64 | if (alignment !== null) { |
64 | return context.column(alignment); | 65 | return context.column(alignment); |
diff --git a/subprojects/frontend/src/language/problem.grammar b/subprojects/frontend/src/language/problem.grammar index ac0b0ea3..313df05d 100644 --- a/subprojects/frontend/src/language/problem.grammar +++ b/subprojects/frontend/src/language/problem.grammar | |||
@@ -1,6 +1,6 @@ | |||
1 | @detectDelim | 1 | @detectDelim |
2 | 2 | ||
3 | @external prop implicitCompletion from '../../../../src/language/props.ts' | 3 | @external prop implicitCompletion from './props' |
4 | 4 | ||
5 | @top Problem { statement* } | 5 | @top Problem { statement* } |
6 | 6 | ||
diff --git a/subprojects/frontend/src/language/problemLanguageSupport.ts b/subprojects/frontend/src/language/problemLanguageSupport.ts index 65fb50dc..246135d8 100644 --- a/subprojects/frontend/src/language/problemLanguageSupport.ts +++ b/subprojects/frontend/src/language/problemLanguageSupport.ts | |||
@@ -7,9 +7,7 @@ import { | |||
7 | LRLanguage, | 7 | LRLanguage, |
8 | } from '@codemirror/language'; | 8 | } from '@codemirror/language'; |
9 | import { styleTags, tags as t } from '@lezer/highlight'; | 9 | import { styleTags, tags as t } from '@lezer/highlight'; |
10 | import { LRParser } from '@lezer/lr'; | ||
11 | 10 | ||
12 | import { parser } from '../../build/generated/sources/lezer/problem'; | ||
13 | import { | 11 | import { |
14 | foldBlockComment, | 12 | foldBlockComment, |
15 | foldConjunction, | 13 | foldConjunction, |
@@ -21,8 +19,9 @@ import { | |||
21 | indentDeclaration, | 19 | indentDeclaration, |
22 | indentPredicateOrRule, | 20 | indentPredicateOrRule, |
23 | } from './indentation'; | 21 | } from './indentation'; |
22 | import { parser } from './problem.grammar'; | ||
24 | 23 | ||
25 | const parserWithMetadata = (parser as LRParser).configure({ | 24 | const parserWithMetadata = parser.configure({ |
26 | props: [ | 25 | props: [ |
27 | styleTags({ | 26 | styleTags({ |
28 | LineComment: t.lineComment, | 27 | LineComment: t.lineComment, |
@@ -86,8 +85,6 @@ const problemLanguage = LRLanguage.define({ | |||
86 | }, | 85 | }, |
87 | }); | 86 | }); |
88 | 87 | ||
89 | export function problemLanguageSupport(): LanguageSupport { | 88 | export default function problemLanguageSupport(): LanguageSupport { |
90 | return new LanguageSupport(problemLanguage, [ | 89 | return new LanguageSupport(problemLanguage, [indentUnit.of(' ')]); |
91 | indentUnit.of(' '), | ||
92 | ]); | ||
93 | } | 90 | } |
diff --git a/subprojects/frontend/src/language/props.ts b/subprojects/frontend/src/language/props.ts index 8e488bf5..65392e75 100644 --- a/subprojects/frontend/src/language/props.ts +++ b/subprojects/frontend/src/language/props.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | /* eslint-disable import/prefer-default-export -- Lezer needs non-default exports */ | ||
2 | |||
1 | import { NodeProp } from '@lezer/common'; | 3 | import { NodeProp } from '@lezer/common'; |
2 | 4 | ||
3 | export const implicitCompletion = new NodeProp({ | 5 | export const implicitCompletion = new NodeProp({ |
diff --git a/subprojects/frontend/src/theme/EditorTheme.ts b/subprojects/frontend/src/theme/EditorTheme.ts index 294192fa..a16b4c3b 100644 --- a/subprojects/frontend/src/theme/EditorTheme.ts +++ b/subprojects/frontend/src/theme/EditorTheme.ts | |||
@@ -1,47 +1,7 @@ | |||
1 | import type { PaletteMode } from '@mui/material'; | 1 | enum EditorTheme { |
2 | |||
3 | import cssVariables from '../themeVariables.module.scss'; | ||
4 | |||
5 | export enum EditorTheme { | ||
6 | Light, | 2 | Light, |
7 | Dark, | 3 | Dark, |
4 | Default = EditorTheme.Dark, | ||
8 | } | 5 | } |
9 | 6 | ||
10 | export class EditorThemeData { | 7 | export default EditorTheme; |
11 | className: string; | ||
12 | |||
13 | paletteMode: PaletteMode; | ||
14 | |||
15 | toggleDarkMode: EditorTheme; | ||
16 | |||
17 | foreground!: string; | ||
18 | |||
19 | foregroundHighlight!: string; | ||
20 | |||
21 | background!: string; | ||
22 | |||
23 | primary!: string; | ||
24 | |||
25 | secondary!: string; | ||
26 | |||
27 | constructor(className: string, paletteMode: PaletteMode, toggleDarkMode: EditorTheme) { | ||
28 | this.className = className; | ||
29 | this.paletteMode = paletteMode; | ||
30 | this.toggleDarkMode = toggleDarkMode; | ||
31 | Reflect.ownKeys(this).forEach((key) => { | ||
32 | if (!Reflect.get(this, key)) { | ||
33 | const cssKey = `${this.className}--${key.toString()}`; | ||
34 | if (cssKey in cssVariables) { | ||
35 | Reflect.set(this, key, cssVariables[cssKey]); | ||
36 | } | ||
37 | } | ||
38 | }); | ||
39 | } | ||
40 | } | ||
41 | |||
42 | export const DEFAULT_THEME = EditorTheme.Dark; | ||
43 | |||
44 | export const EDITOR_THEMES: { [key in EditorTheme]: EditorThemeData } = { | ||
45 | [EditorTheme.Light]: new EditorThemeData('light', 'light', EditorTheme.Dark), | ||
46 | [EditorTheme.Dark]: new EditorThemeData('dark', 'dark', EditorTheme.Light), | ||
47 | }; | ||
diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx index c6194c69..cf18e21c 100644 --- a/subprojects/frontend/src/theme/ThemeProvider.tsx +++ b/subprojects/frontend/src/theme/ThemeProvider.tsx | |||
@@ -1,15 +1,62 @@ | |||
1 | import { | ||
2 | createTheme, | ||
3 | responsiveFontSizes, | ||
4 | type ThemeOptions, | ||
5 | ThemeProvider as MaterialUiThemeProvider, | ||
6 | } from '@mui/material/styles'; | ||
1 | import { observer } from 'mobx-react-lite'; | 7 | import { observer } from 'mobx-react-lite'; |
2 | import { ThemeProvider as MaterialUiThemeProvider } from '@mui/material/styles'; | ||
3 | import React, { type ReactNode } from 'react'; | 8 | import React, { type ReactNode } from 'react'; |
4 | 9 | ||
5 | import { useRootStore } from '../RootStore'; | 10 | import { useRootStore } from '../RootStore'; |
6 | 11 | ||
7 | export const ThemeProvider: React.FC<{ children: ReactNode }> = observer(({ children }) => { | 12 | import EditorTheme from './EditorTheme'; |
8 | const { themeStore } = useRootStore(); | 13 | |
14 | function getMUIThemeOptions(currentTheme: EditorTheme): ThemeOptions { | ||
15 | switch (currentTheme) { | ||
16 | case EditorTheme.Light: | ||
17 | return { | ||
18 | palette: { | ||
19 | primary: { | ||
20 | main: '#56b6c2', | ||
21 | }, | ||
22 | }, | ||
23 | }; | ||
24 | case EditorTheme.Dark: | ||
25 | return { | ||
26 | palette: { | ||
27 | primary: { | ||
28 | main: '#56b6c2', | ||
29 | }, | ||
30 | }, | ||
31 | }; | ||
32 | default: | ||
33 | throw new Error(`Unknown theme: ${currentTheme}`); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | function ThemeProvider({ children }: { children?: ReactNode }) { | ||
38 | const { | ||
39 | themeStore: { currentTheme, darkMode }, | ||
40 | } = useRootStore(); | ||
41 | |||
42 | const themeOptions = getMUIThemeOptions(currentTheme); | ||
43 | const theme = responsiveFontSizes( | ||
44 | createTheme({ | ||
45 | ...themeOptions, | ||
46 | palette: { | ||
47 | mode: darkMode ? 'dark' : 'light', | ||
48 | ...(themeOptions.palette ?? {}), | ||
49 | }, | ||
50 | }), | ||
51 | ); | ||
9 | 52 | ||
10 | return ( | 53 | return ( |
11 | <MaterialUiThemeProvider theme={themeStore.materialUiTheme}> | 54 | <MaterialUiThemeProvider theme={theme}>{children}</MaterialUiThemeProvider> |
12 | {children} | ||
13 | </MaterialUiThemeProvider> | ||
14 | ); | 55 | ); |
15 | }); | 56 | } |
57 | |||
58 | ThemeProvider.defaultProps = { | ||
59 | children: undefined, | ||
60 | }; | ||
61 | |||
62 | export default observer(ThemeProvider); | ||
diff --git a/subprojects/frontend/src/theme/ThemeStore.ts b/subprojects/frontend/src/theme/ThemeStore.ts index ffaf6dde..ded1f29a 100644 --- a/subprojects/frontend/src/theme/ThemeStore.ts +++ b/subprojects/frontend/src/theme/ThemeStore.ts | |||
@@ -1,64 +1,28 @@ | |||
1 | import { makeAutoObservable } from 'mobx'; | 1 | import { makeAutoObservable } from 'mobx'; |
2 | import { | ||
3 | Theme, | ||
4 | createTheme, | ||
5 | responsiveFontSizes, | ||
6 | } from '@mui/material/styles'; | ||
7 | 2 | ||
8 | import { | 3 | import EditorTheme from './EditorTheme'; |
9 | EditorTheme, | ||
10 | EditorThemeData, | ||
11 | DEFAULT_THEME, | ||
12 | EDITOR_THEMES, | ||
13 | } from './EditorTheme'; | ||
14 | 4 | ||
15 | export class ThemeStore { | 5 | export default class ThemeStore { |
16 | currentTheme: EditorTheme = DEFAULT_THEME; | 6 | currentTheme: EditorTheme = EditorTheme.Default; |
17 | 7 | ||
18 | constructor() { | 8 | constructor() { |
19 | makeAutoObservable(this); | 9 | makeAutoObservable(this); |
20 | } | 10 | } |
21 | 11 | ||
22 | toggleDarkMode(): void { | 12 | toggleDarkMode(): void { |
23 | this.currentTheme = this.currentThemeData.toggleDarkMode; | 13 | switch (this.currentTheme) { |
24 | } | 14 | case EditorTheme.Light: |
25 | 15 | this.currentTheme = EditorTheme.Dark; | |
26 | private get currentThemeData(): EditorThemeData { | 16 | break; |
27 | return EDITOR_THEMES[this.currentTheme]; | 17 | case EditorTheme.Dark: |
28 | } | 18 | this.currentTheme = EditorTheme.Light; |
29 | 19 | break; | |
30 | get materialUiTheme(): Theme { | 20 | default: |
31 | const themeData = this.currentThemeData; | 21 | throw new Error(`Unknown theme: ${this.currentTheme}`); |
32 | const materialUiTheme = createTheme({ | 22 | } |
33 | palette: { | ||
34 | mode: themeData.paletteMode, | ||
35 | background: { | ||
36 | default: themeData.background, | ||
37 | paper: themeData.background, | ||
38 | }, | ||
39 | primary: { | ||
40 | main: themeData.primary, | ||
41 | }, | ||
42 | secondary: { | ||
43 | main: themeData.secondary, | ||
44 | }, | ||
45 | error: { | ||
46 | main: themeData.secondary, | ||
47 | }, | ||
48 | text: { | ||
49 | primary: themeData.foregroundHighlight, | ||
50 | secondary: themeData.foreground, | ||
51 | }, | ||
52 | }, | ||
53 | }); | ||
54 | return responsiveFontSizes(materialUiTheme); | ||
55 | } | 23 | } |
56 | 24 | ||
57 | get darkMode(): boolean { | 25 | get darkMode(): boolean { |
58 | return this.currentThemeData.paletteMode === 'dark'; | 26 | return this.currentTheme === EditorTheme.Dark; |
59 | } | ||
60 | |||
61 | get className(): string { | ||
62 | return this.currentThemeData.className; | ||
63 | } | 27 | } |
64 | } | 28 | } |
diff --git a/subprojects/frontend/src/themeVariables.module.scss b/subprojects/frontend/src/themeVariables.module.scss deleted file mode 100644 index 85af4219..00000000 --- a/subprojects/frontend/src/themeVariables.module.scss +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | @import './themes'; | ||
2 | |||
3 | :export { | ||
4 | @each $themeName, $theme in $themes { | ||
5 | @each $variable, $value in $theme { | ||
6 | #{$themeName}--#{$variable}: $value, | ||
7 | } | ||
8 | } | ||
9 | } | ||
diff --git a/subprojects/frontend/src/themes.scss b/subprojects/frontend/src/themes.scss deleted file mode 100644 index a30f1de3..00000000 --- a/subprojects/frontend/src/themes.scss +++ /dev/null | |||
@@ -1,38 +0,0 @@ | |||
1 | $themes: ( | ||
2 | 'dark': ( | ||
3 | 'foreground': #abb2bf, | ||
4 | 'foregroundHighlight': #eeffff, | ||
5 | 'background': #212121, | ||
6 | 'primary': #56b6c2, | ||
7 | 'secondary': #ff5370, | ||
8 | 'keyword': #56b6c2, | ||
9 | 'predicate': #d6e9ff, | ||
10 | 'variable': #c8ae9d, | ||
11 | 'uniqueNode': #d6e9ff, | ||
12 | 'number': #6e88a6, | ||
13 | 'delimiter': #707787, | ||
14 | 'comment': #5c6370, | ||
15 | 'cursor': #56b6c2, | ||
16 | 'selection': #3e4452, | ||
17 | 'currentLine': rgba(0, 0, 0, 0.2), | ||
18 | 'lineNumber': #5c6370, | ||
19 | ), | ||
20 | 'light': ( | ||
21 | 'foreground': #abb2bf, | ||
22 | 'background': #282c34, | ||
23 | 'paper': #21252b, | ||
24 | 'primary': #56b6c2, | ||
25 | 'secondary': #ff5370, | ||
26 | 'keyword': #56b6c2, | ||
27 | 'predicate': #d6e9ff, | ||
28 | 'variable': #c8ae9d, | ||
29 | 'uniqueNode': #d6e9ff, | ||
30 | 'number': #6e88a6, | ||
31 | 'delimiter': #56606d, | ||
32 | 'comment': #55606d, | ||
33 | 'cursor': #f3efe7, | ||
34 | 'selection': #3e4452, | ||
35 | 'currentLine': #2c323c, | ||
36 | 'lineNumber': #5c6370, | ||
37 | ), | ||
38 | ); | ||
diff --git a/subprojects/frontend/src/utils/ConditionVariable.ts b/subprojects/frontend/src/utils/ConditionVariable.ts index 0910dfa6..c8fae9e8 100644 --- a/subprojects/frontend/src/utils/ConditionVariable.ts +++ b/subprojects/frontend/src/utils/ConditionVariable.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { getLogger } from './logger'; | 1 | import PendingTask from './PendingTask'; |
2 | import { PendingTask } from './PendingTask'; | 2 | import getLogger from './getLogger'; |
3 | 3 | ||
4 | const log = getLogger('utils.ConditionVariable'); | 4 | const log = getLogger('utils.ConditionVariable'); |
5 | 5 | ||
6 | export type Condition = () => boolean; | 6 | export type Condition = () => boolean; |
7 | 7 | ||
8 | export class ConditionVariable { | 8 | export default class ConditionVariable { |
9 | condition: Condition; | 9 | condition: Condition; |
10 | 10 | ||
11 | defaultTimeout: number; | 11 | defaultTimeout: number; |
diff --git a/subprojects/frontend/src/utils/PendingTask.ts b/subprojects/frontend/src/utils/PendingTask.ts index 51b79fb0..086993d4 100644 --- a/subprojects/frontend/src/utils/PendingTask.ts +++ b/subprojects/frontend/src/utils/PendingTask.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { getLogger } from './logger'; | 1 | import getLogger from './getLogger'; |
2 | 2 | ||
3 | const log = getLogger('utils.PendingTask'); | 3 | const log = getLogger('utils.PendingTask'); |
4 | 4 | ||
5 | export class PendingTask<T> { | 5 | export default class PendingTask<T> { |
6 | private readonly resolveCallback: (value: T) => void; | 6 | private readonly resolveCallback: (value: T) => void; |
7 | 7 | ||
8 | private readonly rejectCallback: (reason?: unknown) => void; | 8 | private readonly rejectCallback: (reason?: unknown) => void; |
diff --git a/subprojects/frontend/src/utils/Timer.ts b/subprojects/frontend/src/utils/Timer.ts index 8f653070..14e9eb81 100644 --- a/subprojects/frontend/src/utils/Timer.ts +++ b/subprojects/frontend/src/utils/Timer.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export class Timer { | 1 | export default class Timer { |
2 | readonly callback: () => void; | 2 | readonly callback: () => void; |
3 | 3 | ||
4 | readonly defaultTimeout: number; | 4 | readonly defaultTimeout: number; |
diff --git a/subprojects/frontend/src/utils/logger.ts b/subprojects/frontend/src/utils/getLogger.ts index 306d122c..301fd76d 100644 --- a/subprojects/frontend/src/utils/logger.ts +++ b/subprojects/frontend/src/utils/getLogger.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import styles, { CSPair } from 'ansi-styles'; | 1 | import styles, { type CSPair } from 'ansi-styles'; |
2 | import log from 'loglevel'; | 2 | import log from 'loglevel'; |
3 | import * as prefix from 'loglevel-plugin-prefix'; | 3 | import prefix from 'loglevel-plugin-prefix'; |
4 | 4 | ||
5 | const colors: Partial<Record<string, CSPair>> = { | 5 | const colors: Partial<Record<string, CSPair>> = { |
6 | TRACE: styles.magenta, | 6 | TRACE: styles.magenta, |
@@ -12,7 +12,7 @@ const colors: Partial<Record<string, CSPair>> = { | |||
12 | 12 | ||
13 | prefix.reg(log); | 13 | prefix.reg(log); |
14 | 14 | ||
15 | if (DEBUG) { | 15 | if (import.meta.env.DEV) { |
16 | log.setLevel(log.levels.DEBUG); | 16 | log.setLevel(log.levels.DEBUG); |
17 | } else { | 17 | } else { |
18 | log.setLevel(log.levels.WARN); | 18 | log.setLevel(log.levels.WARN); |
@@ -22,10 +22,14 @@ if ('chrome' in window) { | |||
22 | // Only Chromium supports console ANSI escape sequences. | 22 | // Only Chromium supports console ANSI escape sequences. |
23 | prefix.apply(log, { | 23 | prefix.apply(log, { |
24 | format(level, name, timestamp) { | 24 | format(level, name, timestamp) { |
25 | const formattedTimestamp = `${styles.gray.open}[${timestamp.toString()}]${styles.gray.close}`; | 25 | const formattedTimestamp = `${styles.gray.open}[${timestamp.toString()}]${ |
26 | styles.gray.close | ||
27 | }`; | ||
26 | const levelColor = colors[level.toUpperCase()] || styles.red; | 28 | const levelColor = colors[level.toUpperCase()] || styles.red; |
27 | const formattedLevel = `${levelColor.open}${level}${levelColor.close}`; | 29 | const formattedLevel = `${levelColor.open}${level}${levelColor.close}`; |
28 | const formattedName = `${styles.green.open}(${name || 'root'})${styles.green.close}`; | 30 | const formattedName = `${styles.green.open}(${name || 'root'})${ |
31 | styles.green.close | ||
32 | }`; | ||
29 | return `${formattedTimestamp} ${formattedLevel} ${formattedName}`; | 33 | return `${formattedTimestamp} ${formattedLevel} ${formattedName}`; |
30 | }, | 34 | }, |
31 | }); | 35 | }); |
@@ -35,15 +39,17 @@ if ('chrome' in window) { | |||
35 | }); | 39 | }); |
36 | } | 40 | } |
37 | 41 | ||
38 | const appLogger = log.getLogger(PACKAGE_NAME); | 42 | const appLogger = log.getLogger(import.meta.env.VITE_PACKAGE_NAME); |
39 | 43 | ||
40 | appLogger.info('Version:', PACKAGE_NAME, PACKAGE_VERSION); | 44 | appLogger.info( |
41 | appLogger.info('Debug mode:', DEBUG); | 45 | 'Version:', |
46 | import.meta.env.VITE_PACKAGE_NAME, | ||
47 | import.meta.env.VITE_PACKAGE_VERSION, | ||
48 | ); | ||
49 | appLogger.info('Debug mode:', import.meta.env.DEV); | ||
42 | 50 | ||
43 | export function getLoggerFromRoot(name: string | symbol): log.Logger { | 51 | export default function getLogger(name: string | symbol): log.Logger { |
44 | return log.getLogger(name); | 52 | return log.getLogger( |
45 | } | 53 | `${import.meta.env.VITE_PACKAGE_NAME}.${name.toString()}`, |
46 | 54 | ); | |
47 | export function getLogger(name: string | symbol): log.Logger { | ||
48 | return getLoggerFromRoot(`${PACKAGE_NAME}.${name.toString()}`); | ||
49 | } | 55 | } |
diff --git a/subprojects/frontend/src/xtext/ContentAssistService.ts b/subprojects/frontend/src/xtext/ContentAssistService.ts index bedd3b5c..dce2a902 100644 --- a/subprojects/frontend/src/xtext/ContentAssistService.ts +++ b/subprojects/frontend/src/xtext/ContentAssistService.ts | |||
@@ -8,8 +8,9 @@ import type { Transaction } from '@codemirror/state'; | |||
8 | import escapeStringRegexp from 'escape-string-regexp'; | 8 | import escapeStringRegexp from 'escape-string-regexp'; |
9 | 9 | ||
10 | import { implicitCompletion } from '../language/props'; | 10 | import { implicitCompletion } from '../language/props'; |
11 | import type { UpdateService } from './UpdateService'; | 11 | import getLogger from '../utils/getLogger'; |
12 | import { getLogger } from '../utils/logger'; | 12 | |
13 | import type UpdateService from './UpdateService'; | ||
13 | import type { ContentAssistEntry } from './xtextServiceResults'; | 14 | import type { ContentAssistEntry } from './xtextServiceResults'; |
14 | 15 | ||
15 | const PROPOSALS_LIMIT = 1000; | 16 | const PROPOSALS_LIMIT = 1000; |
@@ -48,10 +49,13 @@ function findToken({ pos, state }: CompletionContext): IFoundToken | null { | |||
48 | }; | 49 | }; |
49 | } | 50 | } |
50 | 51 | ||
51 | function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean { | 52 | function shouldCompleteImplicitly( |
52 | return token !== null | 53 | token: IFoundToken | null, |
53 | && token.implicitCompletion | 54 | context: CompletionContext, |
54 | && context.pos - token.from >= 2; | 55 | ): boolean { |
56 | return ( | ||
57 | token !== null && token.implicitCompletion && context.pos - token.from >= 2 | ||
58 | ); | ||
55 | } | 59 | } |
56 | 60 | ||
57 | function computeSpan(prefix: string, entryCount: number): RegExp { | 61 | function computeSpan(prefix: string, entryCount: number): RegExp { |
@@ -78,23 +82,29 @@ function createCompletion(entry: ContentAssistEntry): Completion { | |||
78 | case 'SNIPPET': | 82 | case 'SNIPPET': |
79 | boost = -90; | 83 | boost = -90; |
80 | break; | 84 | break; |
81 | default: { | 85 | default: |
82 | // Penalize qualified names (vs available unqualified names). | 86 | { |
83 | const extraSegments = entry.proposal.match(/::/g)?.length || 0; | 87 | // Penalize qualified names (vs available unqualified names). |
84 | boost = Math.max(-5 * extraSegments, -50); | 88 | const extraSegments = entry.proposal.match(/::/g)?.length || 0; |
85 | } | 89 | boost = Math.max(-5 * extraSegments, -50); |
90 | } | ||
86 | break; | 91 | break; |
87 | } | 92 | } |
88 | return { | 93 | const completion: Completion = { |
89 | label: entry.proposal, | 94 | label: entry.proposal, |
90 | detail: entry.description, | ||
91 | info: entry.documentation, | ||
92 | type: entry.kind?.toLowerCase(), | 95 | type: entry.kind?.toLowerCase(), |
93 | boost, | 96 | boost, |
94 | }; | 97 | }; |
98 | if (entry.documentation !== undefined) { | ||
99 | completion.info = entry.documentation; | ||
100 | } | ||
101 | if (entry.description !== undefined) { | ||
102 | completion.detail = entry.description; | ||
103 | } | ||
104 | return completion; | ||
95 | } | 105 | } |
96 | 106 | ||
97 | export class ContentAssistService { | 107 | export default class ContentAssistService { |
98 | private readonly updateService: UpdateService; | 108 | private readonly updateService: UpdateService; |
99 | 109 | ||
100 | private lastCompletion: CompletionResult | null = null; | 110 | private lastCompletion: CompletionResult | null = null; |
@@ -117,7 +127,7 @@ export class ContentAssistService { | |||
117 | options: [], | 127 | options: [], |
118 | }; | 128 | }; |
119 | } | 129 | } |
120 | let range: { from: number, to: number }; | 130 | let range: { from: number; to: number }; |
121 | let prefix = ''; | 131 | let prefix = ''; |
122 | if (tokenBefore === null) { | 132 | if (tokenBefore === null) { |
123 | range = { | 133 | range = { |
@@ -139,17 +149,20 @@ export class ContentAssistService { | |||
139 | log.trace('Returning cached completion result'); | 149 | log.trace('Returning cached completion result'); |
140 | // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null` | 150 | // Postcondition of `shouldReturnCachedCompletion`: `lastCompletion !== null` |
141 | return { | 151 | return { |
142 | ...this.lastCompletion as CompletionResult, | 152 | ...(this.lastCompletion as CompletionResult), |
143 | ...range, | 153 | ...range, |
144 | }; | 154 | }; |
145 | } | 155 | } |
146 | this.lastCompletion = null; | 156 | this.lastCompletion = null; |
147 | const entries = await this.updateService.fetchContentAssist({ | 157 | const entries = await this.updateService.fetchContentAssist( |
148 | resource: this.updateService.resourceName, | 158 | { |
149 | serviceType: 'assist', | 159 | resource: this.updateService.resourceName, |
150 | caretOffset: context.pos, | 160 | serviceType: 'assist', |
151 | proposalsLimit: PROPOSALS_LIMIT, | 161 | caretOffset: context.pos, |
152 | }, context); | 162 | proposalsLimit: PROPOSALS_LIMIT, |
163 | }, | ||
164 | context, | ||
165 | ); | ||
153 | if (context.aborted) { | 166 | if (context.aborted) { |
154 | return { | 167 | return { |
155 | ...range, | 168 | ...range, |
@@ -175,7 +188,7 @@ export class ContentAssistService { | |||
175 | } | 188 | } |
176 | 189 | ||
177 | private shouldReturnCachedCompletion( | 190 | private shouldReturnCachedCompletion( |
178 | token: { from: number, to: number, text: string } | null, | 191 | token: { from: number; to: number; text: string } | null, |
179 | ): boolean { | 192 | ): boolean { |
180 | if (token === null || this.lastCompletion === null) { | 193 | if (token === null || this.lastCompletion === null) { |
181 | return false; | 194 | return false; |
@@ -185,11 +198,16 @@ export class ContentAssistService { | |||
185 | if (!lastTo) { | 198 | if (!lastTo) { |
186 | return true; | 199 | return true; |
187 | } | 200 | } |
188 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); | 201 | const [transformedFrom, transformedTo] = this.mapRangeInclusive( |
189 | return from >= transformedFrom | 202 | lastFrom, |
190 | && to <= transformedTo | 203 | lastTo, |
191 | && validFor instanceof RegExp | 204 | ); |
192 | && validFor.exec(text) !== null; | 205 | return ( |
206 | from >= transformedFrom && | ||
207 | to <= transformedTo && | ||
208 | validFor instanceof RegExp && | ||
209 | validFor.exec(text) !== null | ||
210 | ); | ||
193 | } | 211 | } |
194 | 212 | ||
195 | private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { | 213 | private shouldInvalidateCachedCompletion(transaction: Transaction): boolean { |
@@ -200,7 +218,10 @@ export class ContentAssistService { | |||
200 | if (!lastTo) { | 218 | if (!lastTo) { |
201 | return true; | 219 | return true; |
202 | } | 220 | } |
203 | const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); | 221 | const [transformedFrom, transformedTo] = this.mapRangeInclusive( |
222 | lastFrom, | ||
223 | lastTo, | ||
224 | ); | ||
204 | let invalidate = false; | 225 | let invalidate = false; |
205 | transaction.changes.iterChangedRanges((fromA, toA) => { | 226 | transaction.changes.iterChangedRanges((fromA, toA) => { |
206 | if (fromA < transformedFrom || toA > transformedTo) { | 227 | if (fromA < transformedFrom || toA > transformedTo) { |
@@ -210,7 +231,10 @@ export class ContentAssistService { | |||
210 | return invalidate; | 231 | return invalidate; |
211 | } | 232 | } |
212 | 233 | ||
213 | private mapRangeInclusive(lastFrom: number, lastTo: number): [number, number] { | 234 | private mapRangeInclusive( |
235 | lastFrom: number, | ||
236 | lastTo: number, | ||
237 | ): [number, number] { | ||
214 | const changes = this.updateService.computeChangesSinceLastUpdate(); | 238 | const changes = this.updateService.computeChangesSinceLastUpdate(); |
215 | const transformedFrom = changes.mapPos(lastFrom); | 239 | const transformedFrom = changes.mapPos(lastFrom); |
216 | const transformedTo = changes.mapPos(lastTo, 1); | 240 | const transformedTo = changes.mapPos(lastTo, 1); |
diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts index dfbb4a19..cf618b96 100644 --- a/subprojects/frontend/src/xtext/HighlightingService.ts +++ b/subprojects/frontend/src/xtext/HighlightingService.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import type { EditorStore } from '../editor/EditorStore'; | 1 | import type EditorStore from '../editor/EditorStore'; |
2 | import type { IHighlightRange } from '../editor/semanticHighlighting'; | 2 | import type { IHighlightRange } from '../editor/semanticHighlighting'; |
3 | import type { UpdateService } from './UpdateService'; | 3 | |
4 | import type UpdateService from './UpdateService'; | ||
4 | import { highlightingResult } from './xtextServiceResults'; | 5 | import { highlightingResult } from './xtextServiceResults'; |
5 | 6 | ||
6 | export class HighlightingService { | 7 | export default class HighlightingService { |
7 | private readonly store: EditorStore; | 8 | private readonly store: EditorStore; |
8 | 9 | ||
9 | private readonly updateService: UpdateService; | 10 | private readonly updateService: UpdateService; |
diff --git a/subprojects/frontend/src/xtext/OccurrencesService.ts b/subprojects/frontend/src/xtext/OccurrencesService.ts index bc865537..21fe8644 100644 --- a/subprojects/frontend/src/xtext/OccurrencesService.ts +++ b/subprojects/frontend/src/xtext/OccurrencesService.ts | |||
@@ -1,15 +1,16 @@ | |||
1 | import { Transaction } from '@codemirror/state'; | 1 | import { Transaction } from '@codemirror/state'; |
2 | 2 | ||
3 | import type { EditorStore } from '../editor/EditorStore'; | 3 | import type EditorStore from '../editor/EditorStore'; |
4 | import type { IOccurrence } from '../editor/findOccurrences'; | 4 | import type { IOccurrence } from '../editor/findOccurrences'; |
5 | import type { UpdateService } from './UpdateService'; | 5 | import Timer from '../utils/Timer'; |
6 | import { getLogger } from '../utils/logger'; | 6 | import getLogger from '../utils/getLogger'; |
7 | import { Timer } from '../utils/Timer'; | 7 | |
8 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | 8 | import type UpdateService from './UpdateService'; |
9 | import type XtextWebSocketClient from './XtextWebSocketClient'; | ||
9 | import { | 10 | import { |
10 | isConflictResult, | 11 | isConflictResult, |
11 | occurrencesResult, | 12 | OccurrencesResult, |
12 | TextRegion, | 13 | type TextRegion, |
13 | } from './xtextServiceResults'; | 14 | } from './xtextServiceResults'; |
14 | 15 | ||
15 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; | 16 | const FIND_OCCURRENCES_TIMEOUT_MS = 1000; |
@@ -33,7 +34,7 @@ function transformOccurrences(regions: TextRegion[]): IOccurrence[] { | |||
33 | return occurrences; | 34 | return occurrences; |
34 | } | 35 | } |
35 | 36 | ||
36 | export class OccurrencesService { | 37 | export default class OccurrencesService { |
37 | private readonly store: EditorStore; | 38 | private readonly store: EditorStore; |
38 | 39 | ||
39 | private readonly webSocketClient: XtextWebSocketClient; | 40 | private readonly webSocketClient: XtextWebSocketClient; |
@@ -94,7 +95,7 @@ export class OccurrencesService { | |||
94 | this.findOccurrencesTimer.schedule(); | 95 | this.findOccurrencesTimer.schedule(); |
95 | return; | 96 | return; |
96 | } | 97 | } |
97 | const parsedOccurrencesResult = occurrencesResult.safeParse(result); | 98 | const parsedOccurrencesResult = OccurrencesResult.safeParse(result); |
98 | if (!parsedOccurrencesResult.success) { | 99 | if (!parsedOccurrencesResult.success) { |
99 | log.error( | 100 | log.error( |
100 | 'Unexpected occurences result', | 101 | 'Unexpected occurences result', |
@@ -107,14 +108,25 @@ export class OccurrencesService { | |||
107 | } | 108 | } |
108 | const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; | 109 | const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data; |
109 | if (stateId !== this.updateService.xtextStateId) { | 110 | if (stateId !== this.updateService.xtextStateId) { |
110 | log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId); | 111 | log.error( |
112 | 'Unexpected state id, expected:', | ||
113 | this.updateService.xtextStateId, | ||
114 | 'got:', | ||
115 | stateId, | ||
116 | ); | ||
111 | this.clearOccurrences(); | 117 | this.clearOccurrences(); |
112 | return; | 118 | return; |
113 | } | 119 | } |
114 | const write = transformOccurrences(writeRegions); | 120 | const write = transformOccurrences(writeRegions); |
115 | const read = transformOccurrences(readRegions); | 121 | const read = transformOccurrences(readRegions); |
116 | this.hasOccurrences = write.length > 0 || read.length > 0; | 122 | this.hasOccurrences = write.length > 0 || read.length > 0; |
117 | log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); | 123 | log.debug( |
124 | 'Found', | ||
125 | write.length, | ||
126 | 'write and', | ||
127 | read.length, | ||
128 | 'read occurrences', | ||
129 | ); | ||
118 | this.store.updateOccurrences(write, read); | 130 | this.store.updateOccurrences(write, read); |
119 | } | 131 | } |
120 | 132 | ||
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index e78944a9..2994b11b 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts | |||
@@ -1,22 +1,23 @@ | |||
1 | import { | 1 | import { |
2 | ChangeDesc, | 2 | type ChangeDesc, |
3 | ChangeSet, | 3 | ChangeSet, |
4 | ChangeSpec, | 4 | type ChangeSpec, |
5 | StateEffect, | 5 | StateEffect, |
6 | Transaction, | 6 | type Transaction, |
7 | } from '@codemirror/state'; | 7 | } from '@codemirror/state'; |
8 | import { nanoid } from 'nanoid'; | 8 | import { nanoid } from 'nanoid'; |
9 | 9 | ||
10 | import type { EditorStore } from '../editor/EditorStore'; | 10 | import type EditorStore from '../editor/EditorStore'; |
11 | import type { XtextWebSocketClient } from './XtextWebSocketClient'; | 11 | import ConditionVariable from '../utils/ConditionVariable'; |
12 | import { ConditionVariable } from '../utils/ConditionVariable'; | 12 | import Timer from '../utils/Timer'; |
13 | import { getLogger } from '../utils/logger'; | 13 | import getLogger from '../utils/getLogger'; |
14 | import { Timer } from '../utils/Timer'; | 14 | |
15 | import type XtextWebSocketClient from './XtextWebSocketClient'; | ||
15 | import { | 16 | import { |
16 | ContentAssistEntry, | 17 | type ContentAssistEntry, |
17 | contentAssistResult, | 18 | ContentAssistResult, |
18 | documentStateResult, | 19 | DocumentStateResult, |
19 | formattingResult, | 20 | FormattingResult, |
20 | isConflictResult, | 21 | isConflictResult, |
21 | } from './xtextServiceResults'; | 22 | } from './xtextServiceResults'; |
22 | 23 | ||
@@ -32,7 +33,7 @@ export interface IAbortSignal { | |||
32 | aborted: boolean; | 33 | aborted: boolean; |
33 | } | 34 | } |
34 | 35 | ||
35 | export class UpdateService { | 36 | export default class UpdateService { |
36 | resourceName: string; | 37 | resourceName: string; |
37 | 38 | ||
38 | xtextStateId: string | null = null; | 39 | xtextStateId: string | null = null; |
@@ -76,8 +77,8 @@ export class UpdateService { | |||
76 | } | 77 | } |
77 | 78 | ||
78 | onTransaction(transaction: Transaction): void { | 79 | onTransaction(transaction: Transaction): void { |
79 | const setDirtyChangesEffect = transaction.effects.find( | 80 | const setDirtyChangesEffect = transaction.effects.find((effect) => |
80 | (effect) => effect.is(setDirtyChanges), | 81 | effect.is(setDirtyChanges), |
81 | ) as StateEffect<ChangeSet> | undefined; | 82 | ) as StateEffect<ChangeSet> | undefined; |
82 | if (setDirtyChangesEffect) { | 83 | if (setDirtyChangesEffect) { |
83 | const { value } = setDirtyChangesEffect; | 84 | const { value } = setDirtyChangesEffect; |
@@ -102,7 +103,10 @@ export class UpdateService { | |||
102 | * @return the summary of changes since the last update | 103 | * @return the summary of changes since the last update |
103 | */ | 104 | */ |
104 | computeChangesSinceLastUpdate(): ChangeDesc { | 105 | computeChangesSinceLastUpdate(): ChangeDesc { |
105 | return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc; | 106 | return ( |
107 | this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || | ||
108 | this.dirtyChanges.desc | ||
109 | ); | ||
106 | } | 110 | } |
107 | 111 | ||
108 | private handleIdleUpdate() { | 112 | private handleIdleUpdate() { |
@@ -131,7 +135,7 @@ export class UpdateService { | |||
131 | serviceType: 'update', | 135 | serviceType: 'update', |
132 | fullText: this.store.state.doc.sliceString(0), | 136 | fullText: this.store.state.doc.sliceString(0), |
133 | }); | 137 | }); |
134 | const { stateId } = documentStateResult.parse(result); | 138 | const { stateId } = DocumentStateResult.parse(result); |
135 | return [stateId, undefined]; | 139 | return [stateId, undefined]; |
136 | } | 140 | } |
137 | 141 | ||
@@ -158,7 +162,7 @@ export class UpdateService { | |||
158 | requiredStateId: this.xtextStateId, | 162 | requiredStateId: this.xtextStateId, |
159 | ...delta, | 163 | ...delta, |
160 | }); | 164 | }); |
161 | const parsedDocumentStateResult = documentStateResult.safeParse(result); | 165 | const parsedDocumentStateResult = DocumentStateResult.safeParse(result); |
162 | if (parsedDocumentStateResult.success) { | 166 | if (parsedDocumentStateResult.success) { |
163 | return [parsedDocumentStateResult.data.stateId, undefined]; | 167 | return [parsedDocumentStateResult.data.stateId, undefined]; |
164 | } | 168 | } |
@@ -197,9 +201,10 @@ export class UpdateService { | |||
197 | requiredStateId: this.xtextStateId, | 201 | requiredStateId: this.xtextStateId, |
198 | ...delta, | 202 | ...delta, |
199 | }); | 203 | }); |
200 | const parsedContentAssistResult = contentAssistResult.safeParse(result); | 204 | const parsedContentAssistResult = ContentAssistResult.safeParse(result); |
201 | if (parsedContentAssistResult.success) { | 205 | if (parsedContentAssistResult.success) { |
202 | const { stateId, entries: resultEntries } = parsedContentAssistResult.data; | 206 | const { stateId, entries: resultEntries } = |
207 | parsedContentAssistResult.data; | ||
203 | return [stateId, resultEntries]; | 208 | return [stateId, resultEntries]; |
204 | } | 209 | } |
205 | if (isConflictResult(result, 'invalidStateId')) { | 210 | if (isConflictResult(result, 'invalidStateId')) { |
@@ -223,14 +228,19 @@ export class UpdateService { | |||
223 | return this.doFetchContentAssist(params, this.xtextStateId as string); | 228 | return this.doFetchContentAssist(params, this.xtextStateId as string); |
224 | } | 229 | } |
225 | 230 | ||
226 | private async doFetchContentAssist(params: Record<string, unknown>, expectedStateId: string) { | 231 | private async doFetchContentAssist( |
232 | params: Record<string, unknown>, | ||
233 | expectedStateId: string, | ||
234 | ) { | ||
227 | const result = await this.webSocketClient.send({ | 235 | const result = await this.webSocketClient.send({ |
228 | ...params, | 236 | ...params, |
229 | requiredStateId: expectedStateId, | 237 | requiredStateId: expectedStateId, |
230 | }); | 238 | }); |
231 | const { stateId, entries } = contentAssistResult.parse(result); | 239 | const { stateId, entries } = ContentAssistResult.parse(result); |
232 | if (stateId !== expectedStateId) { | 240 | if (stateId !== expectedStateId) { |
233 | throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`); | 241 | throw new Error( |
242 | `Unexpected state id, expected: ${expectedStateId} got: ${stateId}`, | ||
243 | ); | ||
234 | } | 244 | } |
235 | return entries; | 245 | return entries; |
236 | } | 246 | } |
@@ -250,7 +260,7 @@ export class UpdateService { | |||
250 | selectionStart: from, | 260 | selectionStart: from, |
251 | selectionEnd: to, | 261 | selectionEnd: to, |
252 | }); | 262 | }); |
253 | const { stateId, formattedText } = formattingResult.parse(result); | 263 | const { stateId, formattedText } = FormattingResult.parse(result); |
254 | this.applyBeforeDirtyChanges({ | 264 | this.applyBeforeDirtyChanges({ |
255 | from, | 265 | from, |
256 | to, | 266 | to, |
@@ -282,16 +292,15 @@ export class UpdateService { | |||
282 | } | 292 | } |
283 | 293 | ||
284 | private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { | 294 | private applyBeforeDirtyChanges(changeSpec: ChangeSpec) { |
285 | const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; | 295 | const pendingChanges = |
296 | this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges; | ||
286 | const revertChanges = pendingChanges.invert(this.store.state.doc); | 297 | const revertChanges = pendingChanges.invert(this.store.state.doc); |
287 | const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); | 298 | const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength); |
288 | const redoChanges = pendingChanges.map(applyBefore.desc); | 299 | const redoChanges = pendingChanges.map(applyBefore.desc); |
289 | const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); | 300 | const changeSet = revertChanges.compose(applyBefore).compose(redoChanges); |
290 | this.store.dispatch({ | 301 | this.store.dispatch({ |
291 | changes: changeSet, | 302 | changes: changeSet, |
292 | effects: [ | 303 | effects: [setDirtyChanges.of(redoChanges)], |
293 | setDirtyChanges.of(redoChanges), | ||
294 | ], | ||
295 | }); | 304 | }); |
296 | } | 305 | } |
297 | 306 | ||
@@ -316,7 +325,9 @@ export class UpdateService { | |||
316 | * @param callback the asynchronous callback that updates the server state | 325 | * @param callback the asynchronous callback that updates the server state |
317 | * @return a promise resolving to the second value returned by `callback` | 326 | * @return a promise resolving to the second value returned by `callback` |
318 | */ | 327 | */ |
319 | private async withUpdate<T>(callback: () => Promise<[string, T]>): Promise<T> { | 328 | private async withUpdate<T>( |
329 | callback: () => Promise<[string, T]>, | ||
330 | ): Promise<T> { | ||
320 | if (this.pendingUpdate !== null) { | 331 | if (this.pendingUpdate !== null) { |
321 | throw new Error('Another update is pending, will not perform update'); | 332 | throw new Error('Another update is pending, will not perform update'); |
322 | } | 333 | } |
diff --git a/subprojects/frontend/src/xtext/ValidationService.ts b/subprojects/frontend/src/xtext/ValidationService.ts index ff7d3700..a0b27251 100644 --- a/subprojects/frontend/src/xtext/ValidationService.ts +++ b/subprojects/frontend/src/xtext/ValidationService.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import type { Diagnostic } from '@codemirror/lint'; | 1 | import type { Diagnostic } from '@codemirror/lint'; |
2 | 2 | ||
3 | import type { EditorStore } from '../editor/EditorStore'; | 3 | import type EditorStore from '../editor/EditorStore'; |
4 | import type { UpdateService } from './UpdateService'; | ||
5 | import { validationResult } from './xtextServiceResults'; | ||
6 | 4 | ||
7 | export class ValidationService { | 5 | import type UpdateService from './UpdateService'; |
6 | import { ValidationResult } from './xtextServiceResults'; | ||
7 | |||
8 | export default class ValidationService { | ||
8 | private readonly store: EditorStore; | 9 | private readonly store: EditorStore; |
9 | 10 | ||
10 | private readonly updateService: UpdateService; | 11 | private readonly updateService: UpdateService; |
@@ -15,15 +16,10 @@ export class ValidationService { | |||
15 | } | 16 | } |
16 | 17 | ||
17 | onPush(push: unknown): void { | 18 | onPush(push: unknown): void { |
18 | const { issues } = validationResult.parse(push); | 19 | const { issues } = ValidationResult.parse(push); |
19 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | 20 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); |
20 | const diagnostics: Diagnostic[] = []; | 21 | const diagnostics: Diagnostic[] = []; |
21 | issues.forEach(({ | 22 | issues.forEach(({ offset, length, severity, description }) => { |
22 | offset, | ||
23 | length, | ||
24 | severity, | ||
25 | description, | ||
26 | }) => { | ||
27 | if (severity === 'ignore') { | 23 | if (severity === 'ignore') { |
28 | return; | 24 | return; |
29 | } | 25 | } |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index 0898e725..7297c674 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -4,19 +4,20 @@ import type { | |||
4 | } from '@codemirror/autocomplete'; | 4 | } from '@codemirror/autocomplete'; |
5 | import type { Transaction } from '@codemirror/state'; | 5 | import type { Transaction } from '@codemirror/state'; |
6 | 6 | ||
7 | import type { EditorStore } from '../editor/EditorStore'; | 7 | import type EditorStore from '../editor/EditorStore'; |
8 | import { ContentAssistService } from './ContentAssistService'; | 8 | import getLogger from '../utils/getLogger'; |
9 | import { HighlightingService } from './HighlightingService'; | 9 | |
10 | import { OccurrencesService } from './OccurrencesService'; | 10 | import ContentAssistService from './ContentAssistService'; |
11 | import { UpdateService } from './UpdateService'; | 11 | import HighlightingService from './HighlightingService'; |
12 | import { getLogger } from '../utils/logger'; | 12 | import OccurrencesService from './OccurrencesService'; |
13 | import { ValidationService } from './ValidationService'; | 13 | import UpdateService from './UpdateService'; |
14 | import { XtextWebSocketClient } from './XtextWebSocketClient'; | 14 | import ValidationService from './ValidationService'; |
15 | import { XtextWebPushService } from './xtextMessages'; | 15 | import XtextWebSocketClient from './XtextWebSocketClient'; |
16 | import type { XtextWebPushService } from './xtextMessages'; | ||
16 | 17 | ||
17 | const log = getLogger('xtext.XtextClient'); | 18 | const log = getLogger('xtext.XtextClient'); |
18 | 19 | ||
19 | export class XtextClient { | 20 | export default class XtextClient { |
20 | private readonly webSocketClient: XtextWebSocketClient; | 21 | private readonly webSocketClient: XtextWebSocketClient; |
21 | 22 | ||
22 | private readonly updateService: UpdateService; | 23 | private readonly updateService: UpdateService; |
@@ -32,11 +33,15 @@ export class XtextClient { | |||
32 | constructor(store: EditorStore) { | 33 | constructor(store: EditorStore) { |
33 | this.webSocketClient = new XtextWebSocketClient( | 34 | this.webSocketClient = new XtextWebSocketClient( |
34 | () => this.updateService.onReconnect(), | 35 | () => this.updateService.onReconnect(), |
35 | (resource, stateId, service, push) => this.onPush(resource, stateId, service, push), | 36 | (resource, stateId, service, push) => |
37 | this.onPush(resource, stateId, service, push), | ||
36 | ); | 38 | ); |
37 | this.updateService = new UpdateService(store, this.webSocketClient); | 39 | this.updateService = new UpdateService(store, this.webSocketClient); |
38 | this.contentAssistService = new ContentAssistService(this.updateService); | 40 | this.contentAssistService = new ContentAssistService(this.updateService); |
39 | this.highlightingService = new HighlightingService(store, this.updateService); | 41 | this.highlightingService = new HighlightingService( |
42 | store, | ||
43 | this.updateService, | ||
44 | ); | ||
40 | this.validationService = new ValidationService(store, this.updateService); | 45 | this.validationService = new ValidationService(store, this.updateService); |
41 | this.occurrencesService = new OccurrencesService( | 46 | this.occurrencesService = new OccurrencesService( |
42 | store, | 47 | store, |
@@ -53,14 +58,29 @@ export class XtextClient { | |||
53 | this.occurrencesService.onTransaction(transaction); | 58 | this.occurrencesService.onTransaction(transaction); |
54 | } | 59 | } |
55 | 60 | ||
56 | private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) { | 61 | private onPush( |
62 | resource: string, | ||
63 | stateId: string, | ||
64 | service: XtextWebPushService, | ||
65 | push: unknown, | ||
66 | ) { | ||
57 | const { resourceName, xtextStateId } = this.updateService; | 67 | const { resourceName, xtextStateId } = this.updateService; |
58 | if (resource !== resourceName) { | 68 | if (resource !== resourceName) { |
59 | log.error('Unknown resource name: expected:', resourceName, 'got:', resource); | 69 | log.error( |
70 | 'Unknown resource name: expected:', | ||
71 | resourceName, | ||
72 | 'got:', | ||
73 | resource, | ||
74 | ); | ||
60 | return; | 75 | return; |
61 | } | 76 | } |
62 | if (stateId !== xtextStateId) { | 77 | if (stateId !== xtextStateId) { |
63 | log.error('Unexpected xtext state id: expected:', xtextStateId, 'got:', stateId); | 78 | log.error( |
79 | 'Unexpected xtext state id: expected:', | ||
80 | xtextStateId, | ||
81 | 'got:', | ||
82 | stateId, | ||
83 | ); | ||
64 | // The current push message might be stale (referring to a previous state), | 84 | // The current push message might be stale (referring to a previous state), |
65 | // so this is not neccessarily an error and there is no need to force-reconnect. | 85 | // so this is not neccessarily an error and there is no need to force-reconnect. |
66 | return; | 86 | return; |
@@ -71,6 +91,9 @@ export class XtextClient { | |||
71 | return; | 91 | return; |
72 | case 'validate': | 92 | case 'validate': |
73 | this.validationService.onPush(push); | 93 | this.validationService.onPush(push); |
94 | return; | ||
95 | default: | ||
96 | throw new Error('Unknown service'); | ||
74 | } | 97 | } |
75 | } | 98 | } |
76 | 99 | ||
diff --git a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts index 2ce20a54..ceb1f3fd 100644 --- a/subprojects/frontend/src/xtext/XtextWebSocketClient.ts +++ b/subprojects/frontend/src/xtext/XtextWebSocketClient.ts | |||
@@ -1,16 +1,17 @@ | |||
1 | import { nanoid } from 'nanoid'; | 1 | import { nanoid } from 'nanoid'; |
2 | 2 | ||
3 | import { getLogger } from '../utils/logger'; | 3 | import PendingTask from '../utils/PendingTask'; |
4 | import { PendingTask } from '../utils/PendingTask'; | 4 | import Timer from '../utils/Timer'; |
5 | import { Timer } from '../utils/Timer'; | 5 | import getLogger from '../utils/getLogger'; |
6 | |||
6 | import { | 7 | import { |
7 | xtextWebErrorResponse, | 8 | XtextWebErrorResponse, |
8 | XtextWebRequest, | 9 | XtextWebRequest, |
9 | xtextWebOkResponse, | 10 | XtextWebOkResponse, |
10 | xtextWebPushMessage, | 11 | XtextWebPushMessage, |
11 | XtextWebPushService, | 12 | XtextWebPushService, |
12 | } from './xtextMessages'; | 13 | } from './xtextMessages'; |
13 | import { pongResult } from './xtextServiceResults'; | 14 | import { PongResult } from './xtextServiceResults'; |
14 | 15 | ||
15 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; | 16 | const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; |
16 | 17 | ||
@@ -18,7 +19,8 @@ const WEBSOCKET_CLOSE_OK = 1000; | |||
18 | 19 | ||
19 | const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; | 20 | const RECONNECT_DELAY_MS = [200, 1000, 5000, 30_000]; |
20 | 21 | ||
21 | const MAX_RECONNECT_DELAY_MS = RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; | 22 | const MAX_RECONNECT_DELAY_MS = |
23 | RECONNECT_DELAY_MS[RECONNECT_DELAY_MS.length - 1]; | ||
22 | 24 | ||
23 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; | 25 | const BACKGROUND_IDLE_TIMEOUT_MS = 5 * 60 * 1000; |
24 | 26 | ||
@@ -47,7 +49,7 @@ enum State { | |||
47 | TimedOut, | 49 | TimedOut, |
48 | } | 50 | } |
49 | 51 | ||
50 | export class XtextWebSocketClient { | 52 | export default class XtextWebSocketClient { |
51 | private nextMessageId = 0; | 53 | private nextMessageId = 0; |
52 | 54 | ||
53 | private connection!: WebSocket; | 55 | private connection!: WebSocket; |
@@ -88,9 +90,11 @@ export class XtextWebSocketClient { | |||
88 | } | 90 | } |
89 | 91 | ||
90 | get isOpen(): boolean { | 92 | get isOpen(): boolean { |
91 | return this.state === State.TabVisible | 93 | return ( |
92 | || this.state === State.TabHiddenIdle | 94 | this.state === State.TabVisible || |
93 | || this.state === State.TabHiddenWaiting; | 95 | this.state === State.TabHiddenIdle || |
96 | this.state === State.TabHiddenWaiting | ||
97 | ); | ||
94 | } | 98 | } |
95 | 99 | ||
96 | private reconnect() { | 100 | private reconnect() { |
@@ -104,7 +108,11 @@ export class XtextWebSocketClient { | |||
104 | this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1); | 108 | this.connection = new WebSocket(webSocketUrl, XTEXT_SUBPROTOCOL_V1); |
105 | this.connection.addEventListener('open', () => { | 109 | this.connection.addEventListener('open', () => { |
106 | if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { | 110 | if (this.connection.protocol !== XTEXT_SUBPROTOCOL_V1) { |
107 | log.error('Unknown subprotocol', this.connection.protocol, 'selected by server'); | 111 | log.error( |
112 | 'Unknown subprotocol', | ||
113 | this.connection.protocol, | ||
114 | 'selected by server', | ||
115 | ); | ||
108 | this.forceReconnectOnError(); | 116 | this.forceReconnectOnError(); |
109 | } | 117 | } |
110 | if (document.visibilityState === 'hidden') { | 118 | if (document.visibilityState === 'hidden') { |
@@ -126,8 +134,11 @@ export class XtextWebSocketClient { | |||
126 | this.handleMessage(event.data); | 134 | this.handleMessage(event.data); |
127 | }); | 135 | }); |
128 | this.connection.addEventListener('close', (event) => { | 136 | this.connection.addEventListener('close', (event) => { |
129 | if (this.isLogicallyClosed && event.code === WEBSOCKET_CLOSE_OK | 137 | if ( |
130 | && this.pendingRequests.size === 0) { | 138 | this.isLogicallyClosed && |
139 | event.code === WEBSOCKET_CLOSE_OK && | ||
140 | this.pendingRequests.size === 0 | ||
141 | ) { | ||
131 | log.info('Websocket closed'); | 142 | log.info('Websocket closed'); |
132 | return; | 143 | return; |
133 | } | 144 | } |
@@ -144,7 +155,10 @@ export class XtextWebSocketClient { | |||
144 | return; | 155 | return; |
145 | } | 156 | } |
146 | this.idleTimer.cancel(); | 157 | this.idleTimer.cancel(); |
147 | if (this.state === State.TabHiddenIdle || this.state === State.TabHiddenWaiting) { | 158 | if ( |
159 | this.state === State.TabHiddenIdle || | ||
160 | this.state === State.TabHiddenWaiting | ||
161 | ) { | ||
148 | this.handleTabVisibleConnected(); | 162 | this.handleTabVisibleConnected(); |
149 | return; | 163 | return; |
150 | } | 164 | } |
@@ -183,7 +197,11 @@ export class XtextWebSocketClient { | |||
183 | this.closeConnection(1000, 'idle timeout'); | 197 | this.closeConnection(1000, 'idle timeout'); |
184 | return; | 198 | return; |
185 | } | 199 | } |
186 | log.info('Waiting for', pending, 'pending requests before closing websocket'); | 200 | log.info( |
201 | 'Waiting for', | ||
202 | pending, | ||
203 | 'pending requests before closing websocket', | ||
204 | ); | ||
187 | } | 205 | } |
188 | 206 | ||
189 | private sendPing() { | 207 | private sendPing() { |
@@ -192,19 +210,21 @@ export class XtextWebSocketClient { | |||
192 | } | 210 | } |
193 | const ping = nanoid(); | 211 | const ping = nanoid(); |
194 | log.trace('Ping', ping); | 212 | log.trace('Ping', ping); |
195 | this.send({ ping }).then((result) => { | 213 | this.send({ ping }) |
196 | const parsedPongResult = pongResult.safeParse(result); | 214 | .then((result) => { |
197 | if (parsedPongResult.success && parsedPongResult.data.pong === ping) { | 215 | const parsedPongResult = PongResult.safeParse(result); |
198 | log.trace('Pong', ping); | 216 | if (parsedPongResult.success && parsedPongResult.data.pong === ping) { |
199 | this.pingTimer.schedule(); | 217 | log.trace('Pong', ping); |
200 | } else { | 218 | this.pingTimer.schedule(); |
201 | log.error('Invalid pong:', parsedPongResult, 'expected:', ping); | 219 | } else { |
220 | log.error('Invalid pong:', parsedPongResult, 'expected:', ping); | ||
221 | this.forceReconnectOnError(); | ||
222 | } | ||
223 | }) | ||
224 | .catch((error) => { | ||
225 | log.error('Error while waiting for ping', error); | ||
202 | this.forceReconnectOnError(); | 226 | this.forceReconnectOnError(); |
203 | } | 227 | }); |
204 | }).catch((error) => { | ||
205 | log.error('Error while waiting for ping', error); | ||
206 | this.forceReconnectOnError(); | ||
207 | }); | ||
208 | } | 228 | } |
209 | 229 | ||
210 | send(request: unknown): Promise<unknown> { | 230 | send(request: unknown): Promise<unknown> { |
@@ -250,13 +270,13 @@ export class XtextWebSocketClient { | |||
250 | this.forceReconnectOnError(); | 270 | this.forceReconnectOnError(); |
251 | return; | 271 | return; |
252 | } | 272 | } |
253 | const okResponse = xtextWebOkResponse.safeParse(message); | 273 | const okResponse = XtextWebOkResponse.safeParse(message); |
254 | if (okResponse.success) { | 274 | if (okResponse.success) { |
255 | const { id, response } = okResponse.data; | 275 | const { id, response } = okResponse.data; |
256 | this.resolveRequest(id, response); | 276 | this.resolveRequest(id, response); |
257 | return; | 277 | return; |
258 | } | 278 | } |
259 | const errorResponse = xtextWebErrorResponse.safeParse(message); | 279 | const errorResponse = XtextWebErrorResponse.safeParse(message); |
260 | if (errorResponse.success) { | 280 | if (errorResponse.success) { |
261 | const { id, error, message: errorMessage } = errorResponse.data; | 281 | const { id, error, message: errorMessage } = errorResponse.data; |
262 | this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); | 282 | this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`)); |
@@ -266,14 +286,9 @@ export class XtextWebSocketClient { | |||
266 | } | 286 | } |
267 | return; | 287 | return; |
268 | } | 288 | } |
269 | const pushMessage = xtextWebPushMessage.safeParse(message); | 289 | const pushMessage = XtextWebPushMessage.safeParse(message); |
270 | if (pushMessage.success) { | 290 | if (pushMessage.success) { |
271 | const { | 291 | const { resource, stateId, service, push } = pushMessage.data; |
272 | resource, | ||
273 | stateId, | ||
274 | service, | ||
275 | push, | ||
276 | } = pushMessage.data; | ||
277 | this.onPush(resource, stateId, service, push); | 292 | this.onPush(resource, stateId, service, push); |
278 | } else { | 293 | } else { |
279 | log.error( | 294 | log.error( |
@@ -343,7 +358,8 @@ export class XtextWebSocketClient { | |||
343 | private handleErrorState() { | 358 | private handleErrorState() { |
344 | this.state = State.Error; | 359 | this.state = State.Error; |
345 | this.reconnectTryCount += 1; | 360 | this.reconnectTryCount += 1; |
346 | const delay = RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS; | 361 | const delay = |
362 | RECONNECT_DELAY_MS[this.reconnectTryCount - 1] || MAX_RECONNECT_DELAY_MS; | ||
347 | log.info('Reconnecting in', delay, 'ms'); | 363 | log.info('Reconnecting in', delay, 'ms'); |
348 | this.reconnectTimer.schedule(delay); | 364 | this.reconnectTimer.schedule(delay); |
349 | } | 365 | } |
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts index 4bf49c17..c4d0c676 100644 --- a/subprojects/frontend/src/xtext/xtextMessages.ts +++ b/subprojects/frontend/src/xtext/xtextMessages.ts | |||
@@ -1,40 +1,42 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ | ||
2 | |||
1 | import { z } from 'zod'; | 3 | import { z } from 'zod'; |
2 | 4 | ||
3 | export const xtextWebRequest = z.object({ | 5 | export const XtextWebRequest = z.object({ |
4 | id: z.string().min(1), | 6 | id: z.string().min(1), |
5 | request: z.unknown(), | 7 | request: z.unknown(), |
6 | }); | 8 | }); |
7 | 9 | ||
8 | export type XtextWebRequest = z.infer<typeof xtextWebRequest>; | 10 | export type XtextWebRequest = z.infer<typeof XtextWebRequest>; |
9 | 11 | ||
10 | export const xtextWebOkResponse = z.object({ | 12 | export const XtextWebOkResponse = z.object({ |
11 | id: z.string().min(1), | 13 | id: z.string().min(1), |
12 | response: z.unknown(), | 14 | response: z.unknown(), |
13 | }); | 15 | }); |
14 | 16 | ||
15 | export type XtextWebOkResponse = z.infer<typeof xtextWebOkResponse>; | 17 | export type XtextWebOkResponse = z.infer<typeof XtextWebOkResponse>; |
16 | 18 | ||
17 | export const xtextWebErrorKind = z.enum(['request', 'server']); | 19 | export const XtextWebErrorKind = z.enum(['request', 'server']); |
18 | 20 | ||
19 | export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>; | 21 | export type XtextWebErrorKind = z.infer<typeof XtextWebErrorKind>; |
20 | 22 | ||
21 | export const xtextWebErrorResponse = z.object({ | 23 | export const XtextWebErrorResponse = z.object({ |
22 | id: z.string().min(1), | 24 | id: z.string().min(1), |
23 | error: xtextWebErrorKind, | 25 | error: XtextWebErrorKind, |
24 | message: z.string(), | 26 | message: z.string(), |
25 | }); | 27 | }); |
26 | 28 | ||
27 | export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>; | 29 | export type XtextWebErrorResponse = z.infer<typeof XtextWebErrorResponse>; |
28 | 30 | ||
29 | export const xtextWebPushService = z.enum(['highlight', 'validate']); | 31 | export const XtextWebPushService = z.enum(['highlight', 'validate']); |
30 | 32 | ||
31 | export type XtextWebPushService = z.infer<typeof xtextWebPushService>; | 33 | export type XtextWebPushService = z.infer<typeof XtextWebPushService>; |
32 | 34 | ||
33 | export const xtextWebPushMessage = z.object({ | 35 | export const XtextWebPushMessage = z.object({ |
34 | resource: z.string().min(1), | 36 | resource: z.string().min(1), |
35 | stateId: z.string().min(1), | 37 | stateId: z.string().min(1), |
36 | service: xtextWebPushService, | 38 | service: XtextWebPushService, |
37 | push: z.unknown(), | 39 | push: z.unknown(), |
38 | }); | 40 | }); |
39 | 41 | ||
40 | export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>; | 42 | export type XtextWebPushMessage = z.infer<typeof XtextWebPushMessage>; |
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index 8b0dbbfb..4cfb9c33 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts | |||
@@ -1,112 +1,120 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */ | ||
2 | |||
1 | import { z } from 'zod'; | 3 | import { z } from 'zod'; |
2 | 4 | ||
3 | export const pongResult = z.object({ | 5 | export const PongResult = z.object({ |
4 | pong: z.string().min(1), | 6 | pong: z.string().min(1), |
5 | }); | 7 | }); |
6 | 8 | ||
7 | export type PongResult = z.infer<typeof pongResult>; | 9 | export type PongResult = z.infer<typeof PongResult>; |
8 | 10 | ||
9 | export const documentStateResult = z.object({ | 11 | export const DocumentStateResult = z.object({ |
10 | stateId: z.string().min(1), | 12 | stateId: z.string().min(1), |
11 | }); | 13 | }); |
12 | 14 | ||
13 | export type DocumentStateResult = z.infer<typeof documentStateResult>; | 15 | export type DocumentStateResult = z.infer<typeof DocumentStateResult>; |
14 | 16 | ||
15 | export const conflict = z.enum(['invalidStateId', 'canceled']); | 17 | export const Conflict = z.enum(['invalidStateId', 'canceled']); |
16 | 18 | ||
17 | export type Conflict = z.infer<typeof conflict>; | 19 | export type Conflict = z.infer<typeof Conflict>; |
18 | 20 | ||
19 | export const serviceConflictResult = z.object({ | 21 | export const ServiceConflictResult = z.object({ |
20 | conflict, | 22 | conflict: Conflict, |
21 | }); | 23 | }); |
22 | 24 | ||
23 | export type ServiceConflictResult = z.infer<typeof serviceConflictResult>; | 25 | export type ServiceConflictResult = z.infer<typeof ServiceConflictResult>; |
24 | 26 | ||
25 | export function isConflictResult(result: unknown, conflictType: Conflict): boolean { | 27 | export function isConflictResult( |
26 | const parsedConflictResult = serviceConflictResult.safeParse(result); | 28 | result: unknown, |
27 | return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType; | 29 | conflictType: Conflict, |
30 | ): boolean { | ||
31 | const parsedConflictResult = ServiceConflictResult.safeParse(result); | ||
32 | return ( | ||
33 | parsedConflictResult.success && | ||
34 | parsedConflictResult.data.conflict === conflictType | ||
35 | ); | ||
28 | } | 36 | } |
29 | 37 | ||
30 | export const severity = z.enum(['error', 'warning', 'info', 'ignore']); | 38 | export const Severity = z.enum(['error', 'warning', 'info', 'ignore']); |
31 | 39 | ||
32 | export type Severity = z.infer<typeof severity>; | 40 | export type Severity = z.infer<typeof Severity>; |
33 | 41 | ||
34 | export const issue = z.object({ | 42 | export const Issue = z.object({ |
35 | description: z.string().min(1), | 43 | description: z.string().min(1), |
36 | severity, | 44 | severity: Severity, |
37 | line: z.number().int(), | 45 | line: z.number().int(), |
38 | column: z.number().int().nonnegative(), | 46 | column: z.number().int().nonnegative(), |
39 | offset: z.number().int().nonnegative(), | 47 | offset: z.number().int().nonnegative(), |
40 | length: z.number().int().nonnegative(), | 48 | length: z.number().int().nonnegative(), |
41 | }); | 49 | }); |
42 | 50 | ||
43 | export type Issue = z.infer<typeof issue>; | 51 | export type Issue = z.infer<typeof Issue>; |
44 | 52 | ||
45 | export const validationResult = z.object({ | 53 | export const ValidationResult = z.object({ |
46 | issues: issue.array(), | 54 | issues: Issue.array(), |
47 | }); | 55 | }); |
48 | 56 | ||
49 | export type ValidationResult = z.infer<typeof validationResult>; | 57 | export type ValidationResult = z.infer<typeof ValidationResult>; |
50 | 58 | ||
51 | export const replaceRegion = z.object({ | 59 | export const ReplaceRegion = z.object({ |
52 | offset: z.number().int().nonnegative(), | 60 | offset: z.number().int().nonnegative(), |
53 | length: z.number().int().nonnegative(), | 61 | length: z.number().int().nonnegative(), |
54 | text: z.string(), | 62 | text: z.string(), |
55 | }); | 63 | }); |
56 | 64 | ||
57 | export type ReplaceRegion = z.infer<typeof replaceRegion>; | 65 | export type ReplaceRegion = z.infer<typeof ReplaceRegion>; |
58 | 66 | ||
59 | export const textRegion = z.object({ | 67 | export const TextRegion = z.object({ |
60 | offset: z.number().int().nonnegative(), | 68 | offset: z.number().int().nonnegative(), |
61 | length: z.number().int().nonnegative(), | 69 | length: z.number().int().nonnegative(), |
62 | }); | 70 | }); |
63 | 71 | ||
64 | export type TextRegion = z.infer<typeof textRegion>; | 72 | export type TextRegion = z.infer<typeof TextRegion>; |
65 | 73 | ||
66 | export const contentAssistEntry = z.object({ | 74 | export const ContentAssistEntry = z.object({ |
67 | prefix: z.string(), | 75 | prefix: z.string(), |
68 | proposal: z.string().min(1), | 76 | proposal: z.string().min(1), |
69 | label: z.string().optional(), | 77 | label: z.string().optional(), |
70 | description: z.string().min(1).optional(), | 78 | description: z.string().min(1).optional(), |
71 | documentation: z.string().min(1).optional(), | 79 | documentation: z.string().min(1).optional(), |
72 | escapePosition: z.number().int().nonnegative().optional(), | 80 | escapePosition: z.number().int().nonnegative().optional(), |
73 | textReplacements: replaceRegion.array(), | 81 | textReplacements: ReplaceRegion.array(), |
74 | editPositions: textRegion.array(), | 82 | editPositions: TextRegion.array(), |
75 | kind: z.string().min(1), | 83 | kind: z.string().min(1), |
76 | }); | 84 | }); |
77 | 85 | ||
78 | export type ContentAssistEntry = z.infer<typeof contentAssistEntry>; | 86 | export type ContentAssistEntry = z.infer<typeof ContentAssistEntry>; |
79 | 87 | ||
80 | export const contentAssistResult = documentStateResult.extend({ | 88 | export const ContentAssistResult = DocumentStateResult.extend({ |
81 | entries: contentAssistEntry.array(), | 89 | entries: ContentAssistEntry.array(), |
82 | }); | 90 | }); |
83 | 91 | ||
84 | export type ContentAssistResult = z.infer<typeof contentAssistResult>; | 92 | export type ContentAssistResult = z.infer<typeof ContentAssistResult>; |
85 | 93 | ||
86 | export const highlightingRegion = z.object({ | 94 | export const HighlightingRegion = z.object({ |
87 | offset: z.number().int().nonnegative(), | 95 | offset: z.number().int().nonnegative(), |
88 | length: z.number().int().nonnegative(), | 96 | length: z.number().int().nonnegative(), |
89 | styleClasses: z.string().min(1).array(), | 97 | styleClasses: z.string().min(1).array(), |
90 | }); | 98 | }); |
91 | 99 | ||
92 | export type HighlightingRegion = z.infer<typeof highlightingRegion>; | 100 | export type HighlightingRegion = z.infer<typeof HighlightingRegion>; |
93 | 101 | ||
94 | export const highlightingResult = z.object({ | 102 | export const highlightingResult = z.object({ |
95 | regions: highlightingRegion.array(), | 103 | regions: HighlightingRegion.array(), |
96 | }); | 104 | }); |
97 | 105 | ||
98 | export type HighlightingResult = z.infer<typeof highlightingResult>; | 106 | export type HighlightingResult = z.infer<typeof highlightingResult>; |
99 | 107 | ||
100 | export const occurrencesResult = documentStateResult.extend({ | 108 | export const OccurrencesResult = DocumentStateResult.extend({ |
101 | writeRegions: textRegion.array(), | 109 | writeRegions: TextRegion.array(), |
102 | readRegions: textRegion.array(), | 110 | readRegions: TextRegion.array(), |
103 | }); | 111 | }); |
104 | 112 | ||
105 | export type OccurrencesResult = z.infer<typeof occurrencesResult>; | 113 | export type OccurrencesResult = z.infer<typeof OccurrencesResult>; |
106 | 114 | ||
107 | export const formattingResult = documentStateResult.extend({ | 115 | export const FormattingResult = DocumentStateResult.extend({ |
108 | formattedText: z.string(), | 116 | formattedText: z.string(), |
109 | replaceRegion: textRegion, | 117 | replaceRegion: TextRegion, |
110 | }); | 118 | }); |
111 | 119 | ||
112 | export type FormattingResult = z.infer<typeof formattingResult>; | 120 | export type FormattingResult = z.infer<typeof FormattingResult>; |
diff --git a/subprojects/frontend/tsconfig.sonar.json b/subprojects/frontend/tsconfig.base.json index 9db12b91..e33e330e 100644 --- a/subprojects/frontend/tsconfig.sonar.json +++ b/subprojects/frontend/tsconfig.base.json | |||
@@ -1,16 +1,15 @@ | |||
1 | { | 1 | { |
2 | "compilerOptions": { | 2 | "compilerOptions": { |
3 | "target": "es2020", | 3 | "target": "ESNext", |
4 | "module": "esnext", | 4 | "module": "ESNext", |
5 | "moduleResolution": "node", | 5 | "moduleResolution": "Node", |
6 | "esModuleInterop": true, | 6 | "esModuleInterop": true, |
7 | "allowSyntheticDefaultImports": true, | 7 | "allowSyntheticDefaultImports": true, |
8 | "jsx": "react", | ||
9 | "strict": true, | 8 | "strict": true, |
10 | "noImplicitOverride": true, | 9 | "noImplicitOverride": true, |
11 | "noImplicitReturns": true, | 10 | "noImplicitReturns": true, |
12 | "noEmit": true, | 11 | "exactOptionalPropertyTypes": true, |
12 | "isolatedModules": true, | ||
13 | "skipLibCheck": true | 13 | "skipLibCheck": true |
14 | }, | 14 | } |
15 | "include": ["./src/**/*"] | ||
16 | } | 15 | } |
diff --git a/subprojects/frontend/tsconfig.json b/subprojects/frontend/tsconfig.json index 94c357c5..fcde9939 100644 --- a/subprojects/frontend/tsconfig.json +++ b/subprojects/frontend/tsconfig.json | |||
@@ -1,18 +1,17 @@ | |||
1 | { | 1 | { |
2 | "extends": "./tsconfig.base.json", | ||
2 | "compilerOptions": { | 3 | "compilerOptions": { |
3 | "target": "es2020", | ||
4 | "module": "esnext", | ||
5 | "moduleResolution": "node", | ||
6 | "esModuleInterop": true, | ||
7 | "allowSyntheticDefaultImports": true, | ||
8 | "jsx": "react", | 4 | "jsx": "react", |
9 | "strict": true, | ||
10 | "noImplicitOverride": true, | ||
11 | "noImplicitReturns": true, | ||
12 | "exactOptionalPropertyTypes": false, | ||
13 | "noEmit": true, | 5 | "noEmit": true, |
14 | "skipLibCheck": true | 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], |
7 | "types": ["vite/client"] | ||
15 | }, | 8 | }, |
16 | "include": ["./src/**/*"], | 9 | "include": [ |
17 | "exclude": ["./build/generated/sources/lezer/*"] | 10 | "src", |
11 | "types" | ||
12 | ], | ||
13 | "exclude": ["types/node"], | ||
14 | "references": [ | ||
15 | { "path": "./tsconfig.node.json" } | ||
16 | ] | ||
18 | } | 17 | } |
diff --git a/subprojects/frontend/tsconfig.node.json b/subprojects/frontend/tsconfig.node.json new file mode 100644 index 00000000..f5d6e6ec --- /dev/null +++ b/subprojects/frontend/tsconfig.node.json | |||
@@ -0,0 +1,17 @@ | |||
1 | { | ||
2 | "extends": "./tsconfig.base.json", | ||
3 | "compilerOptions": { | ||
4 | "composite": true, | ||
5 | "checkJs": true, | ||
6 | "lib": ["ESNext"], | ||
7 | "types": ["node"], | ||
8 | "emitDeclarationOnly": true, | ||
9 | "outDir": "build/typescript" | ||
10 | }, | ||
11 | "include": [ | ||
12 | ".eslintrc.cjs", | ||
13 | "prettier.config.cjs", | ||
14 | "types/node", | ||
15 | "vite.config.ts" | ||
16 | ] | ||
17 | } | ||
diff --git a/subprojects/frontend/types/ImportMeta.d.ts b/subprojects/frontend/types/ImportMeta.d.ts new file mode 100644 index 00000000..2008e268 --- /dev/null +++ b/subprojects/frontend/types/ImportMeta.d.ts | |||
@@ -0,0 +1,9 @@ | |||
1 | interface ImportMeta { | ||
2 | env: { | ||
3 | DEV: boolean; | ||
4 | MODE: string; | ||
5 | PROD: boolean; | ||
6 | VITE_PACKAGE_NAME: string; | ||
7 | VITE_PACKAGE_VERSION: string; | ||
8 | }; | ||
9 | } | ||
diff --git a/subprojects/frontend/types/grammar.d.ts b/subprojects/frontend/types/grammar.d.ts new file mode 100644 index 00000000..1480085b --- /dev/null +++ b/subprojects/frontend/types/grammar.d.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | declare module '*.grammar' { | ||
2 | import type { LRParser } from '@lezer/lr'; | ||
3 | |||
4 | export const parser: LRParser; | ||
5 | } | ||
diff --git a/subprojects/frontend/types/node/@lezer-generator-rollup.d.ts b/subprojects/frontend/types/node/@lezer-generator-rollup.d.ts new file mode 100644 index 00000000..dea39ec9 --- /dev/null +++ b/subprojects/frontend/types/node/@lezer-generator-rollup.d.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | // We have to explicitly redeclare the type of the `./rollup` ESM export of `@lezer/generator`, | ||
2 | // because TypeScript can't find it on its own even with `"moduleResolution": "Node16"`. | ||
3 | declare module '@lezer/generator/rollup' { | ||
4 | import type { Plugin } from 'rollup'; | ||
5 | |||
6 | export function lezer(): Plugin; | ||
7 | } | ||
diff --git a/subprojects/frontend/vite.config.ts b/subprojects/frontend/vite.config.ts new file mode 100644 index 00000000..9cb426cf --- /dev/null +++ b/subprojects/frontend/vite.config.ts | |||
@@ -0,0 +1,92 @@ | |||
1 | import { readFileSync } from 'node:fs'; | ||
2 | import path from 'node:path'; | ||
3 | import { fileURLToPath } from 'node:url'; | ||
4 | |||
5 | import { lezer } from '@lezer/generator/rollup'; | ||
6 | import react from '@vitejs/plugin-react'; | ||
7 | import { defineConfig } from 'vite'; | ||
8 | import injectPreload from 'vite-plugin-inject-preload'; | ||
9 | |||
10 | const thisDir = path.dirname(fileURLToPath(import.meta.url)); | ||
11 | |||
12 | const mode = process.env.MODE || 'development'; | ||
13 | const isDevelopment = mode === 'development'; | ||
14 | |||
15 | function portNumberOrElse(envName: string, fallback: number): number { | ||
16 | const value = process.env[envName]; | ||
17 | return value ? parseInt(value, 10) : fallback; | ||
18 | } | ||
19 | |||
20 | const listenHost = process.env.LISTEN_HOST || 'localhost'; | ||
21 | const listenPort = portNumberOrElse('LISTEN_PORT', 1313); | ||
22 | const apiHost = process.env.API_HOST || listenHost; | ||
23 | const apiPort = portNumberOrElse('API_PORT', 1312); | ||
24 | const apiSecure = apiPort === 443; | ||
25 | const publicHost = process.env.PUBLIC_HOST || listenHost; | ||
26 | const publicPort = portNumberOrElse('PUBLIC_PORT', listenPort); | ||
27 | |||
28 | const { name: packageName, version: packageVersion } = JSON.parse( | ||
29 | readFileSync(path.join(thisDir, 'package.json'), 'utf8'), | ||
30 | ) as { name: string; version: string }; | ||
31 | process.env.VITE_PACKAGE_NAME ??= packageName; | ||
32 | process.env.VITE_PACKAGE_VERSIOn ??= packageVersion; | ||
33 | |||
34 | export default defineConfig({ | ||
35 | logLevel: 'info', | ||
36 | mode, | ||
37 | root: thisDir, | ||
38 | cacheDir: path.join(thisDir, 'build/vite/cache'), | ||
39 | plugins: [ | ||
40 | react({ | ||
41 | babel: { | ||
42 | // Gets rid of deoptimization warnings for large chunks. | ||
43 | // We don't need to minify here, because the output of Babel | ||
44 | // will get passed to esbuild anyways. | ||
45 | compact: false, | ||
46 | minified: false, | ||
47 | }, | ||
48 | }), | ||
49 | injectPreload({ | ||
50 | files: [ | ||
51 | { | ||
52 | match: | ||
53 | /(?:jetbrains-mono-latin-variable-wghtOnly-(?:italic|normal)|roboto-latin-(400|500)-normal).+\.woff2/, | ||
54 | attributes: { | ||
55 | type: 'font/woff2', | ||
56 | as: 'font', | ||
57 | crossorigin: 'anonymous', | ||
58 | }, | ||
59 | }, | ||
60 | ], | ||
61 | }), | ||
62 | lezer(), | ||
63 | ], | ||
64 | base: '', | ||
65 | define: { | ||
66 | __DEV__: JSON.stringify(isDevelopment), // For MobX | ||
67 | }, | ||
68 | build: { | ||
69 | assetsDir: '.', | ||
70 | outDir: path.join('build/vite', mode), | ||
71 | emptyOutDir: true, | ||
72 | sourcemap: isDevelopment, | ||
73 | minify: !isDevelopment, | ||
74 | }, | ||
75 | server: { | ||
76 | host: listenHost, | ||
77 | port: listenPort, | ||
78 | strictPort: true, | ||
79 | proxy: { | ||
80 | '/xtext-service': { | ||
81 | target: `${apiSecure ? 'https' : 'http'}://${apiHost}:${apiPort}`, | ||
82 | ws: true, | ||
83 | secure: apiSecure, | ||
84 | }, | ||
85 | }, | ||
86 | hmr: { | ||
87 | host: publicHost, | ||
88 | clientPort: publicPort, | ||
89 | path: '/vite', | ||
90 | }, | ||
91 | }, | ||
92 | }); | ||
diff --git a/subprojects/frontend/webpack.config.js b/subprojects/frontend/webpack.config.js deleted file mode 100644 index bacb7e4a..00000000 --- a/subprojects/frontend/webpack.config.js +++ /dev/null | |||
@@ -1,164 +0,0 @@ | |||
1 | const fs = require('fs'); | ||
2 | const path = require('path'); | ||
3 | |||
4 | const { DefinePlugin } = require('webpack'); | ||
5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||
6 | const HtmlWebpackInjectPreload = require('@principalstudio/html-webpack-inject-preload'); | ||
7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); | ||
8 | const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity'); | ||
9 | |||
10 | const packageInfo = require('./package.json'); | ||
11 | |||
12 | const currentNodeEnv = process.env.NODE_ENV || 'development'; | ||
13 | const devMode = currentNodeEnv !== 'production'; | ||
14 | const outputPath = path.resolve(__dirname, 'build/webpack', currentNodeEnv); | ||
15 | |||
16 | function portNumberOrElse (envName, fallback) { | ||
17 | const value = process.env[envName]; | ||
18 | return value ? parseInt(value) : fallback; | ||
19 | } | ||
20 | |||
21 | const listenHost = process.env['LISTEN_HOST'] || 'localhost'; | ||
22 | const listenPort = portNumberOrElse('LISTEN_PORT', 1313); | ||
23 | const apiHost = process.env['API_HOST'] || listenHost; | ||
24 | const apiPort = portNumberOrElse('API_PORT', 1312); | ||
25 | const publicHost = process.env['PUBLIC_HOST'] || listenHost; | ||
26 | const publicPort = portNumberOrElse('PUBLIC_PORT', listenPort); | ||
27 | |||
28 | module.exports = { | ||
29 | mode: devMode ? 'development' : 'production', | ||
30 | entry: './src/index', | ||
31 | output: { | ||
32 | path: outputPath, | ||
33 | publicPath: '/', | ||
34 | filename: devMode ? '[name].js' : '[name].[contenthash].js', | ||
35 | assetModuleFilename: devMode ? '[name][ext]' : '[name].[contenthash][ext]', | ||
36 | clean: true, | ||
37 | crossOriginLoading: 'anonymous', | ||
38 | }, | ||
39 | module: { | ||
40 | rules: [ | ||
41 | { | ||
42 | test: /.[jt]sx?$/i, | ||
43 | include: [path.resolve(__dirname, 'src')], | ||
44 | use: [ | ||
45 | { | ||
46 | loader: 'babel-loader', | ||
47 | options: { | ||
48 | presets: [ | ||
49 | [ | ||
50 | '@babel/preset-env', | ||
51 | { | ||
52 | targets: 'defaults', | ||
53 | }, | ||
54 | ], | ||
55 | '@babel/preset-react', | ||
56 | [ | ||
57 | '@babel/preset-typescript', | ||
58 | { | ||
59 | isTSX: true, | ||
60 | allExtensions: true, | ||
61 | allowDeclareFields: true, | ||
62 | onlyRemoveTypeImports: true, | ||
63 | optimizeConstEnums: true, | ||
64 | }, | ||
65 | ] | ||
66 | ], | ||
67 | plugins: [ | ||
68 | '@babel/plugin-transform-runtime', | ||
69 | ], | ||
70 | }, | ||
71 | }, | ||
72 | ], | ||
73 | }, | ||
74 | { | ||
75 | test: /\.scss$/i, | ||
76 | use: [ | ||
77 | devMode ? 'style-loader' : MiniCssExtractPlugin.loader, | ||
78 | 'css-loader', | ||
79 | { | ||
80 | loader: 'sass-loader', | ||
81 | options: { | ||
82 | implementation: require.resolve('sass'), | ||
83 | }, | ||
84 | }, | ||
85 | ], | ||
86 | }, | ||
87 | { | ||
88 | test: /\.(gif|png|jpe?g|svg?)$/i, | ||
89 | use: [ | ||
90 | { | ||
91 | loader: 'image-webpack-loader', | ||
92 | options: { | ||
93 | disable: true, | ||
94 | } | ||
95 | }, | ||
96 | ], | ||
97 | type: 'asset', | ||
98 | }, | ||
99 | { | ||
100 | test: /\.woff2?$/i, | ||
101 | type: 'asset/resource', | ||
102 | }, | ||
103 | ], | ||
104 | }, | ||
105 | resolve: { | ||
106 | extensions: ['.ts', '.tsx', '.js', '.jsx'], | ||
107 | }, | ||
108 | devtool: devMode ? 'inline-source-map' : 'source-map', | ||
109 | optimization: { | ||
110 | providedExports: !devMode, | ||
111 | sideEffects: devMode ? 'flag' : true, | ||
112 | splitChunks: { | ||
113 | chunks: 'all', | ||
114 | }, | ||
115 | }, | ||
116 | devServer: { | ||
117 | client: { | ||
118 | logging: 'info', | ||
119 | overlay: true, | ||
120 | progress: true, | ||
121 | webSocketURL: { | ||
122 | hostname: publicHost, | ||
123 | port: publicPort, | ||
124 | protocol: publicPort === 443 ? 'wss' : 'ws', | ||
125 | }, | ||
126 | }, | ||
127 | compress: true, | ||
128 | host: listenHost, | ||
129 | port: listenPort, | ||
130 | proxy: { | ||
131 | '/xtext-service': { | ||
132 | target: `${apiPort === 443 ? 'https' : 'http'}://${apiHost}:${apiPort}`, | ||
133 | ws: true, | ||
134 | }, | ||
135 | }, | ||
136 | }, | ||
137 | plugins: [ | ||
138 | new DefinePlugin({ | ||
139 | 'DEBUG': JSON.stringify(devMode), | ||
140 | 'PACKAGE_NAME': JSON.stringify(packageInfo.name), | ||
141 | 'PACKAGE_VERSION': JSON.stringify(packageInfo.version), | ||
142 | }), | ||
143 | new MiniCssExtractPlugin({ | ||
144 | filename: '[name].[contenthash].css', | ||
145 | chunkFilename: '[name].[contenthash].css', | ||
146 | }), | ||
147 | new SubresourceIntegrityPlugin(), | ||
148 | new HtmlWebpackPlugin({ | ||
149 | template: 'src/index.html', | ||
150 | }), | ||
151 | new HtmlWebpackInjectPreload({ | ||
152 | files: [ | ||
153 | { | ||
154 | match: /(roboto-latin-(400|500)-normal|jetbrains-mono-latin-variable).*\.woff2/, | ||
155 | attributes: { | ||
156 | as: 'font', | ||
157 | type: 'font/woff2', | ||
158 | crossorigin: 'anonymous', | ||
159 | }, | ||
160 | }, | ||
161 | ], | ||
162 | }), | ||
163 | ], | ||
164 | }; | ||