diff options
author | Kristóf Marussy <kristof@marussy.com> | 2024-01-03 02:13:15 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2024-01-03 13:33:55 +0100 |
commit | 2fe65e414ff3194cdddde01bea6818bbab5290e9 (patch) | |
tree | a597343718059a2ee8727a296e817f997876f248 | |
parent | refactor: matching node names in CLI and web (diff) | |
download | refinery-2fe65e414ff3194cdddde01bea6818bbab5290e9.tar.gz refinery-2fe65e414ff3194cdddde01bea6818bbab5290e9.tar.zst refinery-2fe65e414ff3194cdddde01bea6818bbab5290e9.zip |
feat(web): color identifiers and nodes
We use a palette-based coloring strategy, where each class and enum gets a color
from
11 files changed, 328 insertions, 66 deletions
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 9f560dfb..383e1b75 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -8,10 +8,29 @@ import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; | |||
8 | import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; | 8 | import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; |
9 | import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; | 9 | import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; |
10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; | 10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; |
11 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; | 11 | import { |
12 | alpha, | ||
13 | styled, | ||
14 | type CSSObject, | ||
15 | type Theme, | ||
16 | } from '@mui/material/styles'; | ||
17 | import { range } from 'lodash-es'; | ||
12 | 18 | ||
13 | import svgURL from '../utils/svgURL'; | 19 | import svgURL from '../utils/svgURL'; |
14 | 20 | ||
21 | function createTypeHashStyles(theme: Theme): CSSObject { | ||
22 | const result: CSSObject = {}; | ||
23 | range(theme.palette.highlight.typeHash.length).forEach((i) => { | ||
24 | result[`.tok-problem-typeHash-${i}`] = { | ||
25 | '&, .tok-typeName': { | ||
26 | color: theme.palette.highlight.typeHash[i]?.text, | ||
27 | fontWeight: theme.typography.fontWeightEditorTypeHash, | ||
28 | }, | ||
29 | }; | ||
30 | }); | ||
31 | return result; | ||
32 | } | ||
33 | |||
15 | export default styled('div', { | 34 | export default styled('div', { |
16 | name: 'EditorTheme', | 35 | name: 'EditorTheme', |
17 | shouldForwardProp: (propName) => | 36 | shouldForwardProp: (propName) => |
@@ -124,6 +143,7 @@ export default styled('div', { | |||
124 | fontStyle: 'normal', | 143 | fontStyle: 'normal', |
125 | }, | 144 | }, |
126 | }, | 145 | }, |
146 | ...createTypeHashStyles(theme), | ||
127 | }; | 147 | }; |
128 | 148 | ||
129 | const matchingStyle: CSSObject = { | 149 | const matchingStyle: CSSObject = { |
diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx index 14d58b96..60fd7925 100644 --- a/subprojects/frontend/src/graph/GraphTheme.tsx +++ b/subprojects/frontend/src/graph/GraphTheme.tsx | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -7,7 +7,13 @@ | |||
7 | import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; | 7 | import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; |
8 | import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; | 8 | import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; |
9 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; | 9 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; |
10 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; | 10 | import { |
11 | alpha, | ||
12 | styled, | ||
13 | type CSSObject, | ||
14 | type Theme, | ||
15 | } from '@mui/material/styles'; | ||
16 | import { range } from 'lodash-es'; | ||
11 | 17 | ||
12 | import svgURL from '../utils/svgURL'; | 18 | import svgURL from '../utils/svgURL'; |
13 | 19 | ||
@@ -31,6 +37,18 @@ function createEdgeColor( | |||
31 | }; | 37 | }; |
32 | } | 38 | } |
33 | 39 | ||
40 | function createTypeHashStyles(theme: Theme): CSSObject { | ||
41 | const result: CSSObject = {}; | ||
42 | range(theme.palette.highlight.typeHash.length).forEach((i) => { | ||
43 | result[`.node-typeHash-${i}`] = { | ||
44 | '& [fill="green"]': { | ||
45 | fill: theme.palette.highlight.typeHash[i]?.box, | ||
46 | }, | ||
47 | }; | ||
48 | }); | ||
49 | return result; | ||
50 | } | ||
51 | |||
34 | export default styled('div', { | 52 | export default styled('div', { |
35 | name: 'GraphTheme', | 53 | name: 'GraphTheme', |
36 | })(({ theme }) => ({ | 54 | })(({ theme }) => ({ |
@@ -68,6 +86,7 @@ export default styled('div', { | |||
68 | '.node-exists-UNKNOWN [stroke="black"]': { | 86 | '.node-exists-UNKNOWN [stroke="black"]': { |
69 | strokeDasharray: '5 2', | 87 | strokeDasharray: '5 2', |
70 | }, | 88 | }, |
89 | ...createTypeHashStyles(theme), | ||
71 | '.edge': { | 90 | '.edge': { |
72 | '& text': { | 91 | '& text': { |
73 | fontFamily: theme.typography.fontFamily, | 92 | fontFamily: theme.typography.fontFamily, |
diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts index bd358dfa..3ac5eb1c 100644 --- a/subprojects/frontend/src/graph/dotSource.ts +++ b/subprojects/frontend/src/graph/dotSource.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -142,6 +142,9 @@ function createNodes( | |||
142 | if (data.unaryPredicates.size === 0) { | 142 | if (data.unaryPredicates.size === 0) { |
143 | classList.push('node-empty'); | 143 | classList.push('node-empty'); |
144 | } | 144 | } |
145 | if (node.typeHash !== undefined) { | ||
146 | classList.push(`node-typeHash-${node.typeHash}`); | ||
147 | } | ||
145 | const classes = classList.join(' '); | 148 | const classes = classList.join(' '); |
146 | const name = nodeName(graph, node); | 149 | const name = nodeName(graph, node); |
147 | const border = node.kind === 'INDIVIDUAL' ? 2 : 1; | 150 | const border = node.kind === 'INDIVIDUAL' ? 2 : 1; |
diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx index 18310147..a996cde8 100644 --- a/subprojects/frontend/src/theme/ThemeProvider.tsx +++ b/subprojects/frontend/src/theme/ThemeProvider.tsx | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -24,6 +24,11 @@ interface OuterPalette { | |||
24 | border: string; | 24 | border: string; |
25 | } | 25 | } |
26 | 26 | ||
27 | interface TypeHashPalette { | ||
28 | text: string; | ||
29 | box: string; | ||
30 | } | ||
31 | |||
27 | interface HighlightPalette { | 32 | interface HighlightPalette { |
28 | number: string; | 33 | number: string; |
29 | parameter: string; | 34 | parameter: string; |
@@ -41,17 +46,20 @@ interface HighlightPalette { | |||
41 | selected: string; | 46 | selected: string; |
42 | contrastText: string; | 47 | contrastText: string; |
43 | }; | 48 | }; |
49 | typeHash: TypeHashPalette[]; | ||
44 | } | 50 | } |
45 | 51 | ||
46 | declare module '@mui/material/styles' { | 52 | declare module '@mui/material/styles' { |
47 | interface TypographyVariants { | 53 | interface TypographyVariants { |
48 | fontWeightEditorNormal: number; | 54 | fontWeightEditorNormal: number; |
55 | fontWeightEditorTypeHash: number; | ||
49 | fontWeightEditorBold: number; | 56 | fontWeightEditorBold: number; |
50 | editor: TypographyStyle; | 57 | editor: TypographyStyle; |
51 | } | 58 | } |
52 | 59 | ||
53 | interface TypographyVariantsOptions { | 60 | interface TypographyVariantsOptions { |
54 | fontWeightEditorNormal?: number; | 61 | fontWeightEditorNormal?: number; |
62 | fontWeightEditorTypeHash?: number; | ||
55 | fontWeightEditorBold?: number; | 63 | fontWeightEditorBold?: number; |
56 | editor?: TypographyStyle; | 64 | editor?: TypographyStyle; |
57 | } | 65 | } |
@@ -78,6 +86,7 @@ function createResponsiveTheme( | |||
78 | '"Open Sans Variable", "Open Sans", "Roboto", "Helvetica", "Arial", sans-serif', | 86 | '"Open Sans Variable", "Open Sans", "Roboto", "Helvetica", "Arial", sans-serif', |
79 | fontWeightMedium: 500, | 87 | fontWeightMedium: 500, |
80 | fontWeightEditorNormal: 400, | 88 | fontWeightEditorNormal: 400, |
89 | fontWeightEditorTypeHash: 500, | ||
81 | fontWeightEditorBold: 700, | 90 | fontWeightEditorBold: 700, |
82 | button: { | 91 | button: { |
83 | fontWeight: 600, | 92 | fontWeight: 600, |
@@ -220,7 +229,7 @@ const lightTheme = (() => { | |||
220 | palette: { | 229 | palette: { |
221 | mode: 'light', | 230 | mode: 'light', |
222 | primary: { main: '#038a99' }, | 231 | primary: { main: '#038a99' }, |
223 | secondary: { main: '#e45649' }, | 232 | secondary: { main: '#61afef' }, |
224 | error: { main: '#ca1243' }, | 233 | error: { main: '#ca1243' }, |
225 | warning: { main: '#c18401' }, | 234 | warning: { main: '#c18401' }, |
226 | success: { main: '#50a14f' }, | 235 | success: { main: '#50a14f' }, |
@@ -256,6 +265,18 @@ const lightTheme = (() => { | |||
256 | selected: '#d500f9', | 265 | selected: '#d500f9', |
257 | contrastText: '#fff', | 266 | contrastText: '#fff', |
258 | }, | 267 | }, |
268 | typeHash: [ | ||
269 | { text: '#986801', box: '#e5c07b' }, | ||
270 | { text: '#d6493e', box: '#e06c75' }, | ||
271 | { text: '#50a14f', box: '#98c379' }, | ||
272 | { text: '#a626a4', box: '#c678dd' }, | ||
273 | { text: '#4078f2', box: '#80a7f4' }, | ||
274 | { text: '#827662', box: '#e3d1b2' }, | ||
275 | { text: '#904f53', box: '#e78b8f' }, | ||
276 | { text: '#637855', box: '#abcc94' }, | ||
277 | { text: '#805f89', box: '#dbb2e8' }, | ||
278 | { text: '#5987ae', box: '#92c0e9' }, | ||
279 | ], | ||
259 | }, | 280 | }, |
260 | }, | 281 | }, |
261 | }); | 282 | }); |
@@ -270,6 +291,7 @@ const darkTheme = (() => { | |||
270 | { | 291 | { |
271 | typography: { | 292 | typography: { |
272 | fontWeightEditorNormal: 350, | 293 | fontWeightEditorNormal: 350, |
294 | fontWeightEditorTypeHash: 350, | ||
273 | fontWeightEditorBold: 650, | 295 | fontWeightEditorBold: 650, |
274 | }, | 296 | }, |
275 | palette: { | 297 | palette: { |
@@ -277,7 +299,7 @@ const darkTheme = (() => { | |||
277 | primary: { main: '#56b6c2' }, | 299 | primary: { main: '#56b6c2' }, |
278 | secondary: { main: '#be5046' }, | 300 | secondary: { main: '#be5046' }, |
279 | error: { main: '#e06c75' }, | 301 | error: { main: '#e06c75' }, |
280 | warning: { main: '#e5c07b' }, | 302 | warning: { main: '#d19a66' }, |
281 | success: { main: '#98c379' }, | 303 | success: { main: '#98c379' }, |
282 | info: { main: '#61afef' }, | 304 | info: { main: '#61afef' }, |
283 | background: { | 305 | background: { |
@@ -311,6 +333,18 @@ const darkTheme = (() => { | |||
311 | selected: '#dd33fa', | 333 | selected: '#dd33fa', |
312 | contrastText: darkBackground, | 334 | contrastText: darkBackground, |
313 | }, | 335 | }, |
336 | typeHash: [ | ||
337 | { text: '#e5c07b', box: '#ae8003' }, | ||
338 | { text: '#e06c75', box: '#a23b47' }, | ||
339 | { text: '#98c379', box: '#428141' }, | ||
340 | { text: '#c678dd', box: '#854797' }, | ||
341 | { text: '#61afef', box: '#3982bb' }, | ||
342 | { text: '#e3d1b2', box: '#827662' }, | ||
343 | { text: '#e78b8f', box: '#904f53' }, | ||
344 | { text: '#abcc94', box: '#647e63' }, | ||
345 | { text: '#dbb2e8', box: '#805f89' }, | ||
346 | { text: '#92c0e9', box: '#4f7799' }, | ||
347 | ], | ||
314 | }, | 348 | }, |
315 | }, | 349 | }, |
316 | }, | 350 | }, |
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index e473bd48..792c7de3 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -137,6 +137,7 @@ export type ModelGenerationStartedResult = z.infer< | |||
137 | export const NodeMetadata = z.object({ | 137 | export const NodeMetadata = z.object({ |
138 | name: z.string(), | 138 | name: z.string(), |
139 | simpleName: z.string(), | 139 | simpleName: z.string(), |
140 | typeHash: z.string().optional(), | ||
140 | kind: z.enum(['IMPLICIT', 'INDIVIDUAL', 'NEW']), | 141 | kind: z.enum(['IMPLICIT', 'INDIVIDUAL', 'NEW']), |
141 | }); | 142 | }); |
142 | 143 | ||
@@ -182,15 +183,15 @@ export type SemanticsResult = z.infer<typeof SemanticsResult>; | |||
182 | 183 | ||
183 | export const ModelGenerationResult = z.union([ | 184 | export const ModelGenerationResult = z.union([ |
184 | z.object({ | 185 | z.object({ |
185 | uuid: z.string().nonempty(), | 186 | uuid: z.string().min(1), |
186 | status: z.string(), | 187 | status: z.string(), |
187 | }), | 188 | }), |
188 | z.object({ | 189 | z.object({ |
189 | uuid: z.string().nonempty(), | 190 | uuid: z.string().min(1), |
190 | error: z.string(), | 191 | error: z.string(), |
191 | }), | 192 | }), |
192 | SemanticsSuccessResult.extend({ | 193 | SemanticsSuccessResult.extend({ |
193 | uuid: z.string().nonempty(), | 194 | uuid: z.string().min(1), |
194 | }), | 195 | }), |
195 | ]); | 196 | ]); |
196 | 197 | ||
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java index ae8c70e0..4c775fc6 100644 --- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java +++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -38,6 +38,9 @@ public class ProblemSemanticHighlightingCalculator extends DefaultSemanticHighli | |||
38 | @Inject | 38 | @Inject |
39 | private ProblemDesugarer desugarer; | 39 | private ProblemDesugarer desugarer; |
40 | 40 | ||
41 | @Inject | ||
42 | private TypeHashProvider typeHashProvider; | ||
43 | |||
41 | @Override | 44 | @Override |
42 | protected boolean highlightElement(EObject object, IHighlightedPositionAcceptor acceptor, | 45 | protected boolean highlightElement(EObject object, IHighlightedPositionAcceptor acceptor, |
43 | CancelIndicator cancelIndicator) { | 46 | CancelIndicator cancelIndicator) { |
@@ -127,6 +130,12 @@ public class ProblemSemanticHighlightingCalculator extends DefaultSemanticHighli | |||
127 | classesBuilder.add(NEW_NODE_CLASS); | 130 | classesBuilder.add(NEW_NODE_CLASS); |
128 | } | 131 | } |
129 | } | 132 | } |
133 | if (eObject instanceof Relation relation) { | ||
134 | var typeHash = typeHashProvider.getTypeHash(relation); | ||
135 | if (typeHash != null) { | ||
136 | classesBuilder.add("typeHash-" + typeHash); | ||
137 | } | ||
138 | } | ||
130 | List<String> classes = classesBuilder.build(); | 139 | List<String> classes = classesBuilder.build(); |
131 | return classes.toArray(new String[0]); | 140 | return classes.toArray(new String[0]); |
132 | } | 141 | } |
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java new file mode 100644 index 00000000..f75ecdb2 --- /dev/null +++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java | |||
@@ -0,0 +1,93 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.ide.syntaxcoloring; | ||
7 | |||
8 | import com.google.common.collect.ImmutableMap; | ||
9 | import com.google.inject.Inject; | ||
10 | import com.google.inject.Singleton; | ||
11 | import org.eclipse.xtext.EcoreUtil2; | ||
12 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
13 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
14 | import org.eclipse.xtext.scoping.IScopeProvider; | ||
15 | import org.eclipse.xtext.util.IResourceScopeCache; | ||
16 | import tools.refinery.language.model.problem.*; | ||
17 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | ||
18 | import tools.refinery.language.utils.ProblemUtil; | ||
19 | |||
20 | import java.util.*; | ||
21 | |||
22 | @Singleton | ||
23 | public class TypeHashProvider { | ||
24 | private static final String CACHE_KEY = "tools.refinery.language.ide.syntaxcoloring.TypeHashProvider"; | ||
25 | private static final int COLOR_COUNT = 10; | ||
26 | |||
27 | @Inject | ||
28 | private IResourceScopeCache resourceScopeCache; | ||
29 | |||
30 | @Inject | ||
31 | private IScopeProvider scopeProvider; | ||
32 | |||
33 | @Inject | ||
34 | private IQualifiedNameProvider qualifiedNameProvider; | ||
35 | |||
36 | @Inject | ||
37 | private IQualifiedNameConverter qualifiedNameConverter; | ||
38 | |||
39 | public String getTypeHash(Relation relation) { | ||
40 | if (!(relation instanceof ClassDeclaration || relation instanceof EnumDeclaration) || | ||
41 | ProblemUtil.isBuiltIn(relation)) { | ||
42 | return null; | ||
43 | } | ||
44 | var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(relation); | ||
45 | if (qualifiedName == null) { | ||
46 | return null; | ||
47 | } | ||
48 | var qualifiedNameString = qualifiedNameConverter.toString(qualifiedName); | ||
49 | var problem = EcoreUtil2.getContainerOfType(relation, Problem.class); | ||
50 | if (problem == null) { | ||
51 | return null; | ||
52 | } | ||
53 | var cache = resourceScopeCache.get(CACHE_KEY, problem.eResource(), () -> computeHashes(problem)); | ||
54 | return cache.get(qualifiedNameString); | ||
55 | } | ||
56 | |||
57 | private Map<String, String> computeHashes(Problem problem) { | ||
58 | var qualifiedNameStrings = new TreeSet<String>(); | ||
59 | var scope = scopeProvider.getScope(problem, ProblemPackage.Literals.ASSERTION__RELATION); | ||
60 | for (var description : scope.getAllElements()) { | ||
61 | if (ProblemResourceDescriptionStrategy.COLOR_RELATION_TRUE.equals( | ||
62 | description.getUserData(ProblemResourceDescriptionStrategy.COLOR_RELATION))) { | ||
63 | var qualifiedNameString = qualifiedNameConverter.toString(description.getQualifiedName()); | ||
64 | qualifiedNameStrings.add(qualifiedNameString); | ||
65 | } | ||
66 | } | ||
67 | var stringList = new ArrayList<>(qualifiedNameStrings); | ||
68 | int size = stringList.size(); | ||
69 | if (size == 0) { | ||
70 | return Map.of(); | ||
71 | } | ||
72 | // The use of a non-cryptographic random generator is safe here, because we only use it to shuffle the color | ||
73 | // IDs in a pseudo-random way. The shuffle depends on the size of the list of identifiers before padding to | ||
74 | // make sure that adding a new class randomizes all color IDs. | ||
75 | @SuppressWarnings("squid:S2245") | ||
76 | var random = new Random(size); | ||
77 | int padding = COLOR_COUNT - (size % COLOR_COUNT); | ||
78 | for (int i = 0; i < padding; i++) { | ||
79 | stringList.add(null); | ||
80 | } | ||
81 | size += padding; | ||
82 | Collections.shuffle(stringList, random); | ||
83 | var mapBuilder = ImmutableMap.<String, String>builder(); | ||
84 | for (int i = 0; i < size; i++) { | ||
85 | var key = stringList.get(i); | ||
86 | if (key != null) { | ||
87 | int colorId = i % COLOR_COUNT; | ||
88 | mapBuilder.put(key, Integer.toString(colorId)); | ||
89 | } | ||
90 | } | ||
91 | return mapBuilder.build(); | ||
92 | } | ||
93 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/MetadataCreator.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/MetadataCreator.java index 67ef82ce..f05abc45 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/MetadataCreator.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/MetadataCreator.java | |||
@@ -13,9 +13,7 @@ import org.eclipse.xtext.naming.IQualifiedNameProvider; | |||
13 | import org.eclipse.xtext.naming.QualifiedName; | 13 | import org.eclipse.xtext.naming.QualifiedName; |
14 | import org.eclipse.xtext.scoping.IScope; | 14 | import org.eclipse.xtext.scoping.IScope; |
15 | import org.eclipse.xtext.scoping.IScopeProvider; | 15 | import org.eclipse.xtext.scoping.IScopeProvider; |
16 | import org.jetbrains.annotations.NotNull; | ||
17 | import tools.refinery.language.model.problem.*; | 16 | import tools.refinery.language.model.problem.*; |
18 | import tools.refinery.language.semantics.NodeNameProvider; | ||
19 | import tools.refinery.language.semantics.ProblemTrace; | 17 | import tools.refinery.language.semantics.ProblemTrace; |
20 | import tools.refinery.language.semantics.TracedException; | 18 | import tools.refinery.language.semantics.TracedException; |
21 | import tools.refinery.language.utils.ProblemUtil; | 19 | import tools.refinery.language.utils.ProblemUtil; |
@@ -23,14 +21,11 @@ import tools.refinery.store.model.Model; | |||
23 | import tools.refinery.store.reasoning.ReasoningAdapter; | 21 | import tools.refinery.store.reasoning.ReasoningAdapter; |
24 | import tools.refinery.store.reasoning.literal.Concreteness; | 22 | import tools.refinery.store.reasoning.literal.Concreteness; |
25 | import tools.refinery.store.reasoning.representation.PartialRelation; | 23 | import tools.refinery.store.reasoning.representation.PartialRelation; |
26 | import tools.refinery.store.reasoning.translator.typehierarchy.TypeHierarchyTranslator; | ||
27 | import tools.refinery.store.tuple.Tuple; | ||
28 | 24 | ||
29 | import java.util.ArrayList; | 25 | import java.util.ArrayList; |
30 | import java.util.Collections; | 26 | import java.util.Collections; |
31 | import java.util.Comparator; | 27 | import java.util.Comparator; |
32 | import java.util.List; | 28 | import java.util.List; |
33 | import java.util.function.IntFunction; | ||
34 | 29 | ||
35 | public class MetadataCreator { | 30 | public class MetadataCreator { |
36 | @Inject | 31 | @Inject |
@@ -43,7 +38,7 @@ public class MetadataCreator { | |||
43 | private IQualifiedNameConverter qualifiedNameConverter; | 38 | private IQualifiedNameConverter qualifiedNameConverter; |
44 | 39 | ||
45 | @Inject | 40 | @Inject |
46 | private Provider<NodeNameProvider> nodeNameProviderProvider; | 41 | private Provider<NodeMetadataFactory> nodeMetadataFactoryProvider; |
47 | 42 | ||
48 | private ProblemTrace problemTrace; | 43 | private ProblemTrace problemTrace; |
49 | private IScope nodeScope; | 44 | private IScope nodeScope; |
@@ -63,56 +58,31 @@ public class MetadataCreator { | |||
63 | int nodeCount = model.getAdapter(ReasoningAdapter.class).getNodeCount(); | 58 | int nodeCount = model.getAdapter(ReasoningAdapter.class).getNodeCount(); |
64 | var nodeTrace = problemTrace.getNodeTrace(); | 59 | var nodeTrace = problemTrace.getNodeTrace(); |
65 | var nodes = new NodeMetadata[Math.max(nodeTrace.size(), nodeCount)]; | 60 | var nodes = new NodeMetadata[Math.max(nodeTrace.size(), nodeCount)]; |
66 | var getName = makeGetName(model, concreteness); | 61 | var nodeMetadataFactory = nodeMetadataFactoryProvider.get(); |
62 | nodeMetadataFactory.initialize(problemTrace, concreteness, model); | ||
67 | boolean preserveNewNodes = concreteness == Concreteness.PARTIAL; | 63 | boolean preserveNewNodes = concreteness == Concreteness.PARTIAL; |
68 | for (var entry : nodeTrace.keyValuesView()) { | 64 | for (var entry : nodeTrace.keyValuesView()) { |
69 | var node = entry.getOne(); | 65 | var node = entry.getOne(); |
70 | var id = entry.getTwo(); | 66 | var id = entry.getTwo(); |
71 | nodes[id] = getNodeMetadata(id, node, preserveNewNodes, getName); | 67 | nodes[id] = getNodeMetadata(id, node, preserveNewNodes, nodeMetadataFactory); |
72 | } | 68 | } |
73 | for (int i = 0; i < nodes.length; i++) { | 69 | for (int i = 0; i < nodes.length; i++) { |
74 | if (nodes[i] == null) { | 70 | if (nodes[i] == null) { |
75 | var nodeName = getName.apply(i); | 71 | nodes[i] = nodeMetadataFactory.createFreshlyNamedMetadata(i); |
76 | nodes[i] = new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT); | ||
77 | } | 72 | } |
78 | } | 73 | } |
79 | return List.of(nodes); | 74 | return List.of(nodes); |
80 | } | 75 | } |
81 | 76 | ||
82 | @NotNull | ||
83 | private IntFunction<String> makeGetName(Model model, Concreteness concreteness) { | ||
84 | var nodeNameProvider = nodeNameProviderProvider.get(); | ||
85 | nodeNameProvider.setProblem(problemTrace.getProblem()); | ||
86 | var typeInterpretation = model.getInterpretation(TypeHierarchyTranslator.TYPE_SYMBOL); | ||
87 | var existsInterpretation = model.getAdapter(ReasoningAdapter.class).getPartialInterpretation(concreteness, | ||
88 | ReasoningAdapter.EXISTS_SYMBOL); | ||
89 | return nodeId -> { | ||
90 | var key = Tuple.of(nodeId); | ||
91 | var inferredType = typeInterpretation.get(key); | ||
92 | if (inferredType == null || inferredType.candidateType() == null) { | ||
93 | return nodeNameProvider.getNextName(null); | ||
94 | } | ||
95 | if (concreteness == Concreteness.CANDIDATE && !existsInterpretation.get(key).may()) { | ||
96 | // Do not increment the node name counter for non-existent nodes in the candidate interpretation. | ||
97 | // While non-existent nodes may appear in the partial interpretation, they are never displayed in the | ||
98 | // candidate interpretation. | ||
99 | return "::" + nodeId; | ||
100 | } | ||
101 | var relation = problemTrace.getRelation(inferredType.candidateType()); | ||
102 | return nodeNameProvider.getNextName(relation.getName()); | ||
103 | }; | ||
104 | } | ||
105 | |||
106 | private NodeMetadata getNodeMetadata(int nodeId, Node node, boolean preserveNewNodes, | 77 | private NodeMetadata getNodeMetadata(int nodeId, Node node, boolean preserveNewNodes, |
107 | IntFunction<String> getName) { | 78 | NodeMetadataFactory nodeMetadataFactory) { |
108 | var kind = getNodeKind(node); | 79 | var kind = getNodeKind(node); |
109 | if (!preserveNewNodes && kind == NodeKind.NEW) { | 80 | if (!preserveNewNodes && kind == NodeKind.NEW) { |
110 | var nodeName = getName.apply(nodeId); | 81 | return nodeMetadataFactory.createFreshlyNamedMetadata(nodeId); |
111 | return new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT); | ||
112 | } | 82 | } |
113 | var qualifiedName = getQualifiedName(node); | 83 | var qualifiedName = getQualifiedName(node); |
114 | var simpleName = getSimpleName(node, qualifiedName, nodeScope); | 84 | var simpleName = getSimpleName(node, qualifiedName, nodeScope); |
115 | return new NodeMetadata(qualifiedNameConverter.toString(qualifiedName), | 85 | return nodeMetadataFactory.doCreateMetadata(nodeId, qualifiedNameConverter.toString(qualifiedName), |
116 | qualifiedNameConverter.toString(simpleName), getNodeKind(node)); | 86 | qualifiedNameConverter.toString(simpleName), getNodeKind(node)); |
117 | } | 87 | } |
118 | 88 | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadata.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadata.java index 5da28acf..f3347eac 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadata.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadata.java | |||
@@ -1,9 +1,9 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2023-2023 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.web.semantics.metadata; | 6 | package tools.refinery.language.web.semantics.metadata; |
7 | 7 | ||
8 | public record NodeMetadata(String name, String simpleName, NodeKind kind) implements Metadata { | 8 | public record NodeMetadata(String name, String simpleName, String typeHash, NodeKind kind) implements Metadata { |
9 | } | 9 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadataFactory.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadataFactory.java new file mode 100644 index 00000000..ce0e50c1 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/metadata/NodeMetadataFactory.java | |||
@@ -0,0 +1,89 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.web.semantics.metadata; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import tools.refinery.language.ide.syntaxcoloring.TypeHashProvider; | ||
10 | import tools.refinery.language.semantics.NodeNameProvider; | ||
11 | import tools.refinery.language.semantics.ProblemTrace; | ||
12 | import tools.refinery.store.model.Interpretation; | ||
13 | import tools.refinery.store.model.Model; | ||
14 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
15 | import tools.refinery.store.reasoning.interpretation.PartialInterpretation; | ||
16 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
17 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
18 | import tools.refinery.store.reasoning.translator.typehierarchy.InferredType; | ||
19 | import tools.refinery.store.reasoning.translator.typehierarchy.TypeHierarchyTranslator; | ||
20 | import tools.refinery.store.representation.TruthValue; | ||
21 | import tools.refinery.store.tuple.Tuple; | ||
22 | |||
23 | public class NodeMetadataFactory { | ||
24 | @Inject | ||
25 | private NodeNameProvider nodeNameProvider; | ||
26 | |||
27 | @Inject | ||
28 | private TypeHashProvider typeHashProvider; | ||
29 | |||
30 | private ProblemTrace problemTrace; | ||
31 | private Concreteness concreteness; | ||
32 | private Interpretation<InferredType> typeInterpretation; | ||
33 | private PartialInterpretation<TruthValue, Boolean> existsInterpretation; | ||
34 | |||
35 | public void initialize(ProblemTrace problemTrace, Concreteness concreteness, Model model) { | ||
36 | this.problemTrace = problemTrace; | ||
37 | this.concreteness = concreteness; | ||
38 | typeInterpretation = model.getInterpretation(TypeHierarchyTranslator.TYPE_SYMBOL); | ||
39 | existsInterpretation = model.getAdapter(ReasoningAdapter.class).getPartialInterpretation(concreteness, | ||
40 | ReasoningAdapter.EXISTS_SYMBOL); | ||
41 | nodeNameProvider.setProblem(problemTrace.getProblem()); | ||
42 | } | ||
43 | |||
44 | public NodeMetadata doCreateMetadata(int nodeId, String name, String simpleName, NodeKind kind) { | ||
45 | var type = getType(nodeId); | ||
46 | return doCreateMetadata(name, simpleName, type, kind); | ||
47 | } | ||
48 | |||
49 | public NodeMetadata createFreshlyNamedMetadata(int nodeId) { | ||
50 | var type = getType(nodeId); | ||
51 | var name = getName(type, nodeId); | ||
52 | return doCreateMetadata(name, name, type, NodeKind.IMPLICIT); | ||
53 | } | ||
54 | |||
55 | private PartialRelation getType(int nodeId) { | ||
56 | var inferredType = typeInterpretation.get(Tuple.of(nodeId)); | ||
57 | if (inferredType == null) { | ||
58 | return null; | ||
59 | } | ||
60 | return inferredType.candidateType(); | ||
61 | } | ||
62 | |||
63 | private String getName(PartialRelation type, int nodeId) { | ||
64 | if (concreteness == Concreteness.CANDIDATE && !existsInterpretation.get(Tuple.of(nodeId)).may()) { | ||
65 | // Do not increment the node name counter for non-existent nodes in the candidate interpretation. | ||
66 | // While non-existent nodes may appear in the partial interpretation, they are never displayed in the | ||
67 | // candidate interpretation. | ||
68 | return "::" + nodeId; | ||
69 | } | ||
70 | if (type == null) { | ||
71 | return nodeNameProvider.getNextName(null); | ||
72 | } | ||
73 | var relation = problemTrace.getRelation(type); | ||
74 | return nodeNameProvider.getNextName(relation.getName()); | ||
75 | } | ||
76 | |||
77 | private NodeMetadata doCreateMetadata(String name, String simpleName, PartialRelation type, NodeKind kind) { | ||
78 | var typeHash = getTypeHash(type); | ||
79 | return new NodeMetadata(name, simpleName, typeHash, kind); | ||
80 | } | ||
81 | |||
82 | private String getTypeHash(PartialRelation type) { | ||
83 | if (type == null) { | ||
84 | return null; | ||
85 | } | ||
86 | var relation = problemTrace.getRelation(type); | ||
87 | return typeHashProvider.getTypeHash(relation); | ||
88 | } | ||
89 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java index cac1f265..c04c7d09 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
@@ -28,6 +28,8 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
28 | public static final String ARITY = DATA_PREFIX + "ARITY"; | 28 | public static final String ARITY = DATA_PREFIX + "ARITY"; |
29 | public static final String ERROR_PREDICATE = DATA_PREFIX + "ERROR_PREDICATE"; | 29 | public static final String ERROR_PREDICATE = DATA_PREFIX + "ERROR_PREDICATE"; |
30 | public static final String ERROR_PREDICATE_TRUE = "true"; | 30 | public static final String ERROR_PREDICATE_TRUE = "true"; |
31 | public static final String COLOR_RELATION = DATA_PREFIX + "COLOR_RELATION"; | ||
32 | public static final String COLOR_RELATION_TRUE = "true"; | ||
31 | 33 | ||
32 | @Inject | 34 | @Inject |
33 | private IQualifiedNameConverter qualifiedNameConverter; | 35 | private IQualifiedNameConverter qualifiedNameConverter; |
@@ -44,12 +46,9 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
44 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); | 46 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); |
45 | var problemQualifiedName = getNameAsQualifiedName(problem); | 47 | var problemQualifiedName = getNameAsQualifiedName(problem); |
46 | var userData = getUserData(eObject); | 48 | var userData = getUserData(eObject); |
47 | boolean nameExported; | 49 | QualifiedName lastQualifiedNameToExport = null; |
48 | if (shouldExportSimpleName(eObject)) { | 50 | if (shouldExportSimpleName(eObject)) { |
49 | acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, userData, acceptor); | 51 | lastQualifiedNameToExport = qualifiedName; |
50 | nameExported = true; | ||
51 | } else { | ||
52 | nameExported = false; | ||
53 | } | 52 | } |
54 | var parent = eObject.eContainer(); | 53 | var parent = eObject.eContainer(); |
55 | while (parent != null && parent != problem) { | 54 | while (parent != null && parent != problem) { |
@@ -60,16 +59,18 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
60 | } | 59 | } |
61 | qualifiedName = parentQualifiedName.append(qualifiedName); | 60 | qualifiedName = parentQualifiedName.append(qualifiedName); |
62 | if (shouldExportSimpleName(parent)) { | 61 | if (shouldExportSimpleName(parent)) { |
63 | acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, userData, acceptor); | 62 | if (lastQualifiedNameToExport != null) { |
64 | nameExported = true; | 63 | acceptEObjectDescription(eObject, problemQualifiedName, lastQualifiedNameToExport, userData, |
65 | } else { | 64 | acceptor); |
66 | nameExported = false; | 65 | } |
66 | lastQualifiedNameToExport = qualifiedName; | ||
67 | } | 67 | } |
68 | parent = parent.eContainer(); | 68 | parent = parent.eContainer(); |
69 | } | 69 | } |
70 | if (!nameExported) { | 70 | if (lastQualifiedNameToExport == null) { |
71 | acceptEObjectDescription(eObject, problemQualifiedName, qualifiedName, userData, acceptor); | 71 | lastQualifiedNameToExport = qualifiedName; |
72 | } | 72 | } |
73 | acceptEObjectDescription(eObject, problemQualifiedName, lastQualifiedNameToExport, userData, true, acceptor); | ||
73 | return true; | 74 | return true; |
74 | } | 75 | } |
75 | 76 | ||
@@ -120,8 +121,31 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
120 | 121 | ||
121 | private void acceptEObjectDescription(EObject eObject, QualifiedName prefix, QualifiedName qualifiedName, | 122 | private void acceptEObjectDescription(EObject eObject, QualifiedName prefix, QualifiedName qualifiedName, |
122 | Map<String, String> userData, IAcceptor<IEObjectDescription> acceptor) { | 123 | Map<String, String> userData, IAcceptor<IEObjectDescription> acceptor) { |
124 | acceptEObjectDescription(eObject, prefix, qualifiedName, userData, false, acceptor); | ||
125 | } | ||
126 | |||
127 | private void acceptEObjectDescription(EObject eObject, QualifiedName prefix, QualifiedName qualifiedName, | ||
128 | Map<String, String> userData, boolean fullyQualified, | ||
129 | IAcceptor<IEObjectDescription> acceptor) { | ||
123 | var qualifiedNameWithPrefix = prefix == null ? qualifiedName : prefix.append(qualifiedName); | 130 | var qualifiedNameWithPrefix = prefix == null ? qualifiedName : prefix.append(qualifiedName); |
124 | var description = EObjectDescription.create(qualifiedNameWithPrefix, eObject, userData); | 131 | Map<String, String> userDataWithFullyQualified; |
132 | if (fullyQualified && shouldColorRelation(eObject)) { | ||
133 | userDataWithFullyQualified = ImmutableMap.<String, String>builder() | ||
134 | .putAll(userData) | ||
135 | .put(COLOR_RELATION, COLOR_RELATION_TRUE) | ||
136 | .build(); | ||
137 | } else { | ||
138 | userDataWithFullyQualified = userData; | ||
139 | } | ||
140 | var description = EObjectDescription.create(qualifiedNameWithPrefix, eObject, userDataWithFullyQualified); | ||
125 | acceptor.accept(description); | 141 | acceptor.accept(description); |
126 | } | 142 | } |
143 | |||
144 | private boolean shouldColorRelation(EObject eObject) { | ||
145 | if (ProblemUtil.isBuiltIn(eObject)) { | ||
146 | return false; | ||
147 | } | ||
148 | return eObject instanceof ClassDeclaration || eObject instanceof EnumDeclaration; | ||
149 | |||
150 | } | ||
127 | } | 151 | } |