aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2024-03-22 03:19:29 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2024-03-22 03:19:29 +0100
commitddd0e0746c30ccf0bfd9b32b6225cd2ce3eb240c (patch)
tree34806446ca566bebb6524a95b1337fd1280fcf13
parentrefactor(web): improve web app styling (diff)
downloadrefinery-ddd0e0746c30ccf0bfd9b32b6225cd2ce3eb240c.tar.gz
refinery-ddd0e0746c30ccf0bfd9b32b6225cd2ce3eb240c.tar.zst
refinery-ddd0e0746c30ccf0bfd9b32b6225cd2ce3eb240c.zip
feat: custom identifier coloring
-rw-r--r--subprojects/frontend/package.json2
-rw-r--r--subprojects/frontend/src/editor/EditorArea.tsx1
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts8
-rw-r--r--subprojects/frontend/src/editor/EditorTheme.ts36
-rw-r--r--subprojects/frontend/src/graph/DotGraphVisualizer.tsx8
-rw-r--r--subprojects/frontend/src/graph/GraphStore.ts36
-rw-r--r--subprojects/frontend/src/graph/GraphTheme.tsx31
-rw-r--r--subprojects/frontend/src/graph/dotSource.ts3
-rw-r--r--subprojects/frontend/src/graph/export/exportDiagram.tsx11
-rw-r--r--subprojects/frontend/src/graph/obfuscateColor.ts21
-rw-r--r--subprojects/frontend/src/xtext/HighlightingService.ts12
-rw-r--r--subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java24
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java8
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java42
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java6
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java94
-rw-r--r--yarn.lock11
17 files changed, 334 insertions, 20 deletions
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json
index 73bb463d..970d00a3 100644
--- a/subprojects/frontend/package.json
+++ b/subprojects/frontend/package.json
@@ -55,6 +55,7 @@
55 "ansi-styles": "^6.2.1", 55 "ansi-styles": "^6.2.1",
56 "csstype": "^3.1.3", 56 "csstype": "^3.1.3",
57 "d3": "^7.8.5", 57 "d3": "^7.8.5",
58 "d3-color": "^3.1.0",
58 "d3-graphviz": "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch", 59 "d3-graphviz": "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch",
59 "d3-selection": "^3.0.0", 60 "d3-selection": "^3.0.0",
60 "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", 61 "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch",
@@ -78,6 +79,7 @@
78 "devDependencies": { 79 "devDependencies": {
79 "@lezer/generator": "^1.6.0", 80 "@lezer/generator": "^1.6.0",
80 "@types/d3": "^7.4.3", 81 "@types/d3": "^7.4.3",
82 "@types/d3-color": "^3.1.3",
81 "@types/d3-graphviz": "^2.6.10", 83 "@types/d3-graphviz": "^2.6.10",
82 "@types/d3-selection": "^3.0.10", 84 "@types/d3-selection": "^3.0.10",
83 "@types/d3-zoom": "^3.0.8", 85 "@types/d3-zoom": "^3.0.8",
diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx
index aafaad40..ae5cff34 100644
--- a/subprojects/frontend/src/editor/EditorArea.tsx
+++ b/subprojects/frontend/src/editor/EditorArea.tsx
@@ -39,6 +39,7 @@ export default observer(function EditorArea({
39 showLineNumbers={editorStore.showLineNumbers} 39 showLineNumbers={editorStore.showLineNumbers}
40 showActiveLine={!editorStore.hasSelection} 40 showActiveLine={!editorStore.hasSelection}
41 colorIdentifiers={editorStore.colorIdentifiers} 41 colorIdentifiers={editorStore.colorIdentifiers}
42 hexTypeHashes={editorStore.hexTypeHashes}
42 ref={editorParentRef} 43 ref={editorParentRef}
43 /> 44 />
44 </Box> 45 </Box>
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index 33bca382..f128d70d 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -111,6 +111,8 @@ export default class EditorStore {
111 111
112 unsavedChanges = false; 112 unsavedChanges = false;
113 113
114 hexTypeHashes: string[] = [];
115
114 constructor( 116 constructor(
115 initialValue: string, 117 initialValue: string,
116 pwaStore: PWAStore, 118 pwaStore: PWAStore,
@@ -275,8 +277,12 @@ export default class EditorStore {
275 this.doCommand(nextDiagnostic); 277 this.doCommand(nextDiagnostic);
276 } 278 }
277 279
278 updateSemanticHighlighting(ranges: IHighlightRange[]): void { 280 updateSemanticHighlighting(
281 ranges: IHighlightRange[],
282 hexTypeHashes: string[],
283 ): void {
279 this.dispatch(setSemanticHighlighting(ranges)); 284 this.dispatch(setSemanticHighlighting(ranges));
285 this.hexTypeHashes = hexTypeHashes;
280 } 286 }
281 287
282 updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void { 288 updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void {
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts
index 4978c7f7..6deda080 100644
--- a/subprojects/frontend/src/editor/EditorTheme.ts
+++ b/subprojects/frontend/src/editor/EditorTheme.ts
@@ -14,6 +14,7 @@ import {
14 type CSSObject, 14 type CSSObject,
15 type Theme, 15 type Theme,
16} from '@mui/material/styles'; 16} from '@mui/material/styles';
17import { lch } from 'd3-color';
17import { range } from 'lodash-es'; 18import { range } from 'lodash-es';
18 19
19import svgURL from '../utils/svgURL'; 20import svgURL from '../utils/svgURL';
@@ -21,6 +22,7 @@ import svgURL from '../utils/svgURL';
21function createTypeHashStyles( 22function createTypeHashStyles(
22 theme: Theme, 23 theme: Theme,
23 colorIdentifiers: boolean, 24 colorIdentifiers: boolean,
25 hexTypeHashes: string[],
24): CSSObject { 26): CSSObject {
25 if (!colorIdentifiers) { 27 if (!colorIdentifiers) {
26 return {}; 28 return {};
@@ -34,6 +36,26 @@ function createTypeHashStyles(
34 }, 36 },
35 }; 37 };
36 }); 38 });
39 hexTypeHashes.forEach((typeHash) => {
40 let color = lch(`#${typeHash}`);
41 if (theme.palette.mode === 'dark') {
42 color = color.brighter();
43 if (color.l < 60) {
44 color.l = 60;
45 }
46 } else {
47 color = color.darker();
48 if (color.l > 60) {
49 color.l = 60;
50 }
51 }
52 result[`.tok-problem-typeHash-_${typeHash}`] = {
53 '&, .tok-typeName': {
54 color: color.formatRgb(),
55 fontWeight: theme.typography.fontWeightEditorTypeHash,
56 },
57 };
58 });
37 return result; 59 return result;
38} 60}
39 61
@@ -42,12 +64,20 @@ export default styled('div', {
42 shouldForwardProp: (propName) => 64 shouldForwardProp: (propName) =>
43 propName !== 'showLineNumbers' && 65 propName !== 'showLineNumbers' &&
44 propName !== 'showActiveLine' && 66 propName !== 'showActiveLine' &&
45 propName !== 'colorIdentifiers', 67 propName !== 'colorIdentifiers' &&
68 propName !== 'hexTypeHashes',
46})<{ 69})<{
47 showLineNumbers: boolean; 70 showLineNumbers: boolean;
48 showActiveLine: boolean; 71 showActiveLine: boolean;
49 colorIdentifiers: boolean; 72 colorIdentifiers: boolean;
50}>(({ theme, showLineNumbers, showActiveLine, colorIdentifiers }) => { 73 hexTypeHashes: string[];
74}>(({
75 theme,
76 showLineNumbers,
77 showActiveLine,
78 colorIdentifiers,
79 hexTypeHashes,
80}) => {
51 const editorFontStyle: CSSObject = { 81 const editorFontStyle: CSSObject = {
52 ...theme.typography.editor, 82 ...theme.typography.editor,
53 fontWeight: theme.typography.fontWeightEditorNormal, 83 fontWeight: theme.typography.fontWeightEditorNormal,
@@ -157,7 +187,7 @@ export default styled('div', {
157 fontStyle: 'normal', 187 fontStyle: 'normal',
158 }, 188 },
159 }, 189 },
160 ...createTypeHashStyles(theme, colorIdentifiers), 190 ...createTypeHashStyles(theme, colorIdentifiers, hexTypeHashes),
161 }; 191 };
162 192
163 const matchingStyle: CSSObject = { 193 const matchingStyle: CSSObject = {
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
index cc8b5116..0980ea20 100644
--- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
+++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
@@ -154,7 +154,13 @@ function DotGraphVisualizer({
154 ], 154 ],
155 ); 155 );
156 156
157 return <GraphTheme ref={setElement} colorNodes={graph.colorNodes} />; 157 return (
158 <GraphTheme
159 ref={setElement}
160 colorNodes={graph.colorNodes}
161 hexTypeHashes={graph.hexTypeHashes}
162 />
163 );
158} 164}
159 165
160DotGraphVisualizer.defaultProps = { 166DotGraphVisualizer.defaultProps = {
diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts
index d9282326..301b4d86 100644
--- a/subprojects/frontend/src/graph/GraphStore.ts
+++ b/subprojects/frontend/src/graph/GraphStore.ts
@@ -49,6 +49,8 @@ export function isVisibilityAllowed(
49 return true; 49 return true;
50} 50}
51 51
52const TYPE_HASH_HEX_PREFFIX = '_';
53
52export default class GraphStore { 54export default class GraphStore {
53 semantics: SemanticsSuccessResult = { 55 semantics: SemanticsSuccessResult = {
54 nodes: [], 56 nodes: [],
@@ -66,6 +68,10 @@ export default class GraphStore {
66 68
67 selectedSymbol: RelationMetadata | undefined; 69 selectedSymbol: RelationMetadata | undefined;
68 70
71 hexTypeHashes: string[] = [];
72
73 private typeHashesMap = new Map<string, number>();
74
69 constructor( 75 constructor(
70 private readonly editorStore: EditorStore, 76 private readonly editorStore: EditorStore,
71 private readonly nameOverride?: string, 77 private readonly nameOverride?: string,
@@ -188,6 +194,36 @@ export default class GraphStore {
188 this.visibility.delete(key); 194 this.visibility.delete(key);
189 }); 195 });
190 this.setSelectedSymbol(this.selectedSymbol); 196 this.setSelectedSymbol(this.selectedSymbol);
197 this.updateTypeHashes();
198 }
199
200 /**
201 * Maintains a list of past and current color codes to avoid flashing
202 * when the graph view updates.
203 *
204 * As long as the previously used colors are still in in `typeHashesMap`,
205 * the view will not flash while Graphviz is recomputing, because we'll
206 * keep emitting styles for the colors.
207 */
208 private updateTypeHashes(): void {
209 this.semantics.nodes.forEach(({ typeHash }) => {
210 if (
211 typeHash !== undefined &&
212 typeHash.startsWith(TYPE_HASH_HEX_PREFFIX)
213 ) {
214 const key = typeHash.substring(TYPE_HASH_HEX_PREFFIX.length);
215 this.typeHashesMap.set(key, 0);
216 }
217 });
218 this.hexTypeHashes = Array.from(this.typeHashesMap.keys());
219 this.hexTypeHashes.forEach((typeHash) => {
220 const age = this.typeHashesMap.get(typeHash);
221 if (age !== undefined && age < 10) {
222 this.typeHashesMap.set(typeHash, age + 1);
223 } else {
224 this.typeHashesMap.delete(typeHash);
225 }
226 });
191 } 227 }
192 228
193 get colorNodes(): boolean { 229 get colorNodes(): boolean {
diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx
index 34954345..50a003e0 100644
--- a/subprojects/frontend/src/graph/GraphTheme.tsx
+++ b/subprojects/frontend/src/graph/GraphTheme.tsx
@@ -13,10 +13,13 @@ import {
13 type CSSObject, 13 type CSSObject,
14 type Theme, 14 type Theme,
15} from '@mui/material/styles'; 15} from '@mui/material/styles';
16import { lch } from 'd3-color';
16import { range } from 'lodash-es'; 17import { range } from 'lodash-es';
17 18
18import svgURL from '../utils/svgURL'; 19import svgURL from '../utils/svgURL';
19 20
21import obfuscateColor from './obfuscateColor';
22
20function createEdgeColor( 23function createEdgeColor(
21 suffix: string, 24 suffix: string,
22 stroke: string, 25 stroke: string,
@@ -37,16 +40,32 @@ function createEdgeColor(
37 }; 40 };
38} 41}
39 42
40function createTypeHashStyles(theme: Theme, colorNodes: boolean): CSSObject { 43function createTypeHashStyles(
44 theme: Theme,
45 colorNodes: boolean,
46 typeHashes: string[],
47): CSSObject {
41 if (!colorNodes) { 48 if (!colorNodes) {
42 return {}; 49 return {};
43 } 50 }
44 const result: CSSObject = {}; 51 const result: CSSObject = {};
45 range(theme.palette.highlight.typeHash.length).forEach((i) => { 52 range(theme.palette.highlight.typeHash.length).forEach((i) => {
46 result[`.node-typeHash-${i} .node-header`] = { 53 result[`.node-typeHash-${obfuscateColor(i.toString(10))} .node-header`] = {
47 fill: theme.palette.highlight.typeHash[i]?.box, 54 fill: theme.palette.highlight.typeHash[i]?.box,
48 }; 55 };
49 }); 56 });
57 typeHashes.forEach((typeHash) => {
58 let color = lch(`#${typeHash}`);
59 if (theme.palette.mode === 'dark') {
60 color = color.darker();
61 if (color.l > 50) {
62 color.l = 50;
63 }
64 }
65 result[`.node-typeHash-_${obfuscateColor(typeHash)} .node-header`] = {
66 fill: color.formatRgb(),
67 };
68 });
50 return result; 69 return result;
51} 70}
52 71
@@ -69,10 +88,12 @@ function iconStyle(
69export function createGraphTheme({ 88export function createGraphTheme({
70 theme, 89 theme,
71 colorNodes, 90 colorNodes,
91 hexTypeHashes,
72 noEmbedIcons, 92 noEmbedIcons,
73}: { 93}: {
74 theme: Theme; 94 theme: Theme;
75 colorNodes: boolean; 95 colorNodes: boolean;
96 hexTypeHashes: string[];
76 noEmbedIcons?: boolean; 97 noEmbedIcons?: boolean;
77}): CSSObject { 98}): CSSObject {
78 const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24; 99 const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24;
@@ -111,7 +132,7 @@ export function createGraphTheme({
111 '.node-exists-UNKNOWN .node-outline': { 132 '.node-exists-UNKNOWN .node-outline': {
112 strokeDasharray: '5 2', 133 strokeDasharray: '5 2',
113 }, 134 },
114 ...createTypeHashStyles(theme, colorNodes), 135 ...createTypeHashStyles(theme, colorNodes, hexTypeHashes),
115 '.edge': { 136 '.edge': {
116 '& text': { 137 '& text': {
117 fontFamily: theme.typography.fontFamily, 138 fontFamily: theme.typography.fontFamily,
@@ -155,7 +176,9 @@ export function createGraphTheme({
155 176
156export default styled('div', { 177export default styled('div', {
157 name: 'GraphTheme', 178 name: 'GraphTheme',
158})<{ colorNodes: boolean }>((args) => ({ 179 shouldForwardProp: (prop) =>
180 prop !== 'colorNodes' && prop !== 'hexTypeHashes',
181})<{ colorNodes: boolean; hexTypeHashes: string[] }>((args) => ({
159 '& svg': { 182 '& svg': {
160 userSelect: 'none', 183 userSelect: 'none',
161 ...createGraphTheme(args), 184 ...createGraphTheme(args),
diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts
index 3ac5eb1c..bcd386cf 100644
--- a/subprojects/frontend/src/graph/dotSource.ts
+++ b/subprojects/frontend/src/graph/dotSource.ts
@@ -10,6 +10,7 @@ import type {
10} from '../xtext/xtextServiceResults'; 10} from '../xtext/xtextServiceResults';
11 11
12import type GraphStore from './GraphStore'; 12import type GraphStore from './GraphStore';
13import obfuscateColor from './obfuscateColor';
13 14
14const EDGE_WEIGHT = 1; 15const EDGE_WEIGHT = 1;
15const CONTAINMENT_WEIGHT = 5; 16const CONTAINMENT_WEIGHT = 5;
@@ -143,7 +144,7 @@ function createNodes(
143 classList.push('node-empty'); 144 classList.push('node-empty');
144 } 145 }
145 if (node.typeHash !== undefined) { 146 if (node.typeHash !== undefined) {
146 classList.push(`node-typeHash-${node.typeHash}`); 147 classList.push(`node-typeHash-${obfuscateColor(node.typeHash)}`);
147 } 148 }
148 const classes = classList.join(' '); 149 const classes = classList.join(' ');
149 const name = nodeName(graph, node); 150 const name = nodeName(graph, node);
diff --git a/subprojects/frontend/src/graph/export/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx
index 44489d28..6abbcfdf 100644
--- a/subprojects/frontend/src/graph/export/exportDiagram.tsx
+++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx
@@ -147,6 +147,7 @@ function appendStyles(
147 svg: SVGSVGElement, 147 svg: SVGSVGElement,
148 theme: Theme, 148 theme: Theme,
149 colorNodes: boolean, 149 colorNodes: boolean,
150 hexTypeHashes: string[],
150 fontsCSS: string, 151 fontsCSS: string,
151): void { 152): void {
152 const cache = createCache({ 153 const cache = createCache({
@@ -159,6 +160,7 @@ function appendStyles(
159 const styles = serializeStyles([createGraphTheme], cache.registered, { 160 const styles = serializeStyles([createGraphTheme], cache.registered, {
160 theme, 161 theme,
161 colorNodes, 162 colorNodes,
163 hexTypeHashes,
162 noEmbedIcons: true, 164 noEmbedIcons: true,
163 }); 165 });
164 const rules: string[] = [fontsCSS]; 166 const rules: string[] = [fontsCSS];
@@ -336,7 +338,14 @@ export default async function exportDiagram(
336 } else if (settings.format === 'svg' && settings.embedFonts) { 338 } else if (settings.format === 'svg' && settings.embedFonts) {
337 fontsCSS = await fetchFontCSS(); 339 fontsCSS = await fetchFontCSS();
338 } 340 }
339 appendStyles(svgDocument, copyOfSVG, theme, colorNodes, fontsCSS); 341 appendStyles(
342 svgDocument,
343 copyOfSVG,
344 theme,
345 colorNodes,
346 graph.hexTypeHashes,
347 fontsCSS,
348 );
340 349
341 if (settings.format === 'pdf') { 350 if (settings.format === 'pdf') {
342 const pdf = await serializePDF(copyOfSVG, settings); 351 const pdf = await serializePDF(copyOfSVG, settings);
diff --git a/subprojects/frontend/src/graph/obfuscateColor.ts b/subprojects/frontend/src/graph/obfuscateColor.ts
new file mode 100644
index 00000000..57c15804
--- /dev/null
+++ b/subprojects/frontend/src/graph/obfuscateColor.ts
@@ -0,0 +1,21 @@
1/*
2 * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7const regExp = /\d/g;
8const offset = 'g'.charCodeAt(0) - '0'.charCodeAt(0);
9
10/*
11 * The SVG animation framework we use garbles all numbers while interpolating,
12 * so we mask numbers in hex color codes by replacing them with letters.
13 *
14 * @param color The hex code.
15 * @return The hex code with no number characters.
16 */
17export default function obfuscateColor(color: string): string {
18 return color.replaceAll(regExp, (match) =>
19 String.fromCharCode(match.charCodeAt(0) + offset),
20 );
21}
diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts
index 447f1401..eacee117 100644
--- a/subprojects/frontend/src/xtext/HighlightingService.ts
+++ b/subprojects/frontend/src/xtext/HighlightingService.ts
@@ -10,6 +10,8 @@ import type { IHighlightRange } from '../editor/semanticHighlighting';
10import type UpdateService from './UpdateService'; 10import type UpdateService from './UpdateService';
11import { highlightingResult } from './xtextServiceResults'; 11import { highlightingResult } from './xtextServiceResults';
12 12
13const TYPE_HASH_HEX_PREFIX = 'typeHash-_';
14
13export default class HighlightingService { 15export default class HighlightingService {
14 constructor( 16 constructor(
15 private readonly store: EditorStore, 17 private readonly store: EditorStore,
@@ -20,6 +22,7 @@ export default class HighlightingService {
20 const { regions } = highlightingResult.parse(push); 22 const { regions } = highlightingResult.parse(push);
21 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 23 const allChanges = this.updateService.computeChangesSinceLastUpdate();
22 const ranges: IHighlightRange[] = []; 24 const ranges: IHighlightRange[] = [];
25 const hexTypeHashes = new Set<string>();
23 regions.forEach(({ offset, length, styleClasses }) => { 26 regions.forEach(({ offset, length, styleClasses }) => {
24 if (styleClasses.length === 0) { 27 if (styleClasses.length === 0) {
25 return; 28 return;
@@ -34,11 +37,16 @@ export default class HighlightingService {
34 to, 37 to,
35 classes: styleClasses, 38 classes: styleClasses,
36 }); 39 });
40 styleClasses.forEach((styleClass) => {
41 if (styleClass.startsWith(TYPE_HASH_HEX_PREFIX)) {
42 hexTypeHashes.add(styleClass.substring(TYPE_HASH_HEX_PREFIX.length));
43 }
44 });
37 }); 45 });
38 this.store.updateSemanticHighlighting(ranges); 46 this.store.updateSemanticHighlighting(ranges, Array.from(hexTypeHashes));
39 } 47 }
40 48
41 onDisconnect(): void { 49 onDisconnect(): void {
42 this.store.updateSemanticHighlighting([]); 50 this.store.updateSemanticHighlighting([], []);
43 } 51 }
44} 52}
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
index dd9f1053..82a6af06 100644
--- 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
@@ -5,16 +5,17 @@
5 */ 5 */
6package tools.refinery.language.ide.syntaxcoloring; 6package tools.refinery.language.ide.syntaxcoloring;
7 7
8import com.google.common.collect.ImmutableMap;
9import com.google.inject.Inject; 8import com.google.inject.Inject;
10import com.google.inject.Singleton; 9import com.google.inject.Singleton;
11import org.eclipse.xtext.EcoreUtil2; 10import org.eclipse.xtext.EcoreUtil2;
12import org.eclipse.xtext.naming.IQualifiedNameConverter; 11import org.eclipse.xtext.naming.IQualifiedNameConverter;
13import org.eclipse.xtext.naming.IQualifiedNameProvider; 12import org.eclipse.xtext.naming.IQualifiedNameProvider;
13import org.eclipse.xtext.resource.IEObjectDescription;
14import org.eclipse.xtext.resource.IResourceDescription; 14import org.eclipse.xtext.resource.IResourceDescription;
15import org.eclipse.xtext.scoping.IScopeProvider; 15import org.eclipse.xtext.scoping.IScopeProvider;
16import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; 16import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider;
17import org.eclipse.xtext.util.IResourceScopeCache; 17import org.eclipse.xtext.util.IResourceScopeCache;
18import tools.refinery.language.documentation.DocumentationCommentParser;
18import tools.refinery.language.model.problem.*; 19import tools.refinery.language.model.problem.*;
19import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; 20import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
20import tools.refinery.language.scoping.imports.ImportCollector; 21import tools.refinery.language.scoping.imports.ImportCollector;
@@ -65,21 +66,30 @@ public class TypeHashProvider {
65 66
66 private Map<String, String> computeHashes(Problem problem) { 67 private Map<String, String> computeHashes(Problem problem) {
67 var resourceDescriptions = getResourceDescriptions(problem); 68 var resourceDescriptions = getResourceDescriptions(problem);
69 var map = new HashMap<String, String>();
68 var qualifiedNameStrings = new TreeSet<String>(); 70 var qualifiedNameStrings = new TreeSet<String>();
69 for (var resourceDescription : resourceDescriptions) { 71 for (var resourceDescription : resourceDescriptions) {
70 for (var description : resourceDescription.getExportedObjectsByType(ProblemPackage.Literals.RELATION)) { 72 for (var description : resourceDescription.getExportedObjectsByType(ProblemPackage.Literals.RELATION)) {
71 if (ProblemResourceDescriptionStrategy.COLOR_RELATION_TRUE.equals( 73 if (ProblemResourceDescriptionStrategy.COLOR_RELATION_TRUE.equals(
72 description.getUserData(ProblemResourceDescriptionStrategy.COLOR_RELATION))) { 74 description.getUserData(ProblemResourceDescriptionStrategy.COLOR_RELATION))) {
73 var qualifiedNameString = qualifiedNameConverter.toString(description.getQualifiedName()); 75 var qualifiedNameString = qualifiedNameConverter.toString(description.getQualifiedName());
76 var presetColor = getPresetColor(description);
77 if (presetColor != null) {
78 map.put(qualifiedNameString, presetColor);
79 }
74 qualifiedNameStrings.add(qualifiedNameString); 80 qualifiedNameStrings.add(qualifiedNameString);
75 } 81 }
76 } 82 }
77 } 83 }
78 var stringList = new ArrayList<>(qualifiedNameStrings); 84 var stringList = new ArrayList<>(qualifiedNameStrings);
79 int size = stringList.size(); 85 int size = stringList.size();
80 if (size == 0) { 86 if (size != 0) {
81 return Map.of(); 87 shuffleColors(size, stringList, map);
82 } 88 }
89 return Collections.unmodifiableMap(map);
90 }
91
92 private static void shuffleColors(int size, ArrayList<String> stringList, Map<String, String> map) {
83 // The use of a non-cryptographic random generator is safe here, because we only use it to shuffle the color 93 // The use of a non-cryptographic random generator is safe here, because we only use it to shuffle the color
84 // IDs in a pseudo-random way. The shuffle depends on the size of the list of identifiers before padding to 94 // IDs in a pseudo-random way. The shuffle depends on the size of the list of identifiers before padding to
85 // make sure that adding a new class randomizes all color IDs. 95 // make sure that adding a new class randomizes all color IDs.
@@ -91,15 +101,13 @@ public class TypeHashProvider {
91 } 101 }
92 size += padding; 102 size += padding;
93 Collections.shuffle(stringList, random); 103 Collections.shuffle(stringList, random);
94 var mapBuilder = ImmutableMap.<String, String>builder();
95 for (int i = 0; i < size; i++) { 104 for (int i = 0; i < size; i++) {
96 var key = stringList.get(i); 105 var key = stringList.get(i);
97 if (key != null) { 106 if (key != null) {
98 int colorId = i % COLOR_COUNT; 107 int colorId = i % COLOR_COUNT;
99 mapBuilder.put(key, Integer.toString(colorId)); 108 map.putIfAbsent(key, Integer.toString(colorId));
100 } 109 }
101 } 110 }
102 return mapBuilder.build();
103 } 111 }
104 112
105 private List<IResourceDescription> getResourceDescriptions(Problem problem) { 113 private List<IResourceDescription> getResourceDescriptions(Problem problem) {
@@ -127,4 +135,8 @@ public class TypeHashProvider {
127 } 135 }
128 return resourceDescriptions; 136 return resourceDescriptions;
129 } 137 }
138
139 private String getPresetColor(IEObjectDescription description) {
140 return description.getUserData(DocumentationCommentParser.COLOR_TAG);
141 }
130} 142}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
index f9a564b0..f7039027 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
@@ -12,6 +12,7 @@ package tools.refinery.language;
12import com.google.inject.Binder; 12import com.google.inject.Binder;
13import com.google.inject.name.Names; 13import com.google.inject.name.Names;
14import org.eclipse.xtext.conversion.IValueConverterService; 14import org.eclipse.xtext.conversion.IValueConverterService;
15import org.eclipse.xtext.documentation.impl.AbstractMultiLineCommentProvider;
15import org.eclipse.xtext.linking.ILinkingService; 16import org.eclipse.xtext.linking.ILinkingService;
16import org.eclipse.xtext.naming.IQualifiedNameConverter; 17import org.eclipse.xtext.naming.IQualifiedNameConverter;
17import org.eclipse.xtext.naming.IQualifiedNameProvider; 18import org.eclipse.xtext.naming.IQualifiedNameProvider;
@@ -144,4 +145,11 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
144 public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() { 145 public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() {
145 return ProblemDiagnosticConverter.class; 146 return ProblemDiagnosticConverter.class;
146 } 147 }
148
149 public void configureAbstractMultiLineCommentProvider(Binder binder) {
150 // Only parse documentation tags from Javadoc-style comments.
151 binder.bind(String.class)
152 .annotatedWith(Names.named(AbstractMultiLineCommentProvider.START_TAG))
153 .toInstance("/\\*\\*");
154 }
147} 155}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java b/subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java
new file mode 100644
index 00000000..3586abd2
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java
@@ -0,0 +1,42 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.documentation;
7
8import com.google.inject.Inject;
9import com.google.inject.Singleton;
10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.xtext.documentation.IEObjectDocumentationProvider;
12
13import java.util.Locale;
14import java.util.Map;
15import java.util.regex.Pattern;
16
17@Singleton
18public class DocumentationCommentParser {
19 private static final String PREFIX = "tools.refinery.language.documentation.DocumentationCommentParser.";
20 public static final String COLOR_TAG = PREFIX + "COLOR_TAG";
21
22 private static final Pattern COLOR_PATTERN = Pattern.compile(
23 "(?m)^@color[ \t]+(\\d|#[\\da-fA-F]{6}|#[\\da-fA-F]{3})");
24
25 @Inject
26 private IEObjectDocumentationProvider documentationProvider;
27
28 public Map<String, String> parseDocumentation(EObject eObject) {
29 var documentation = documentationProvider.getDocumentation(eObject);
30 if (documentation == null) {
31 return Map.of();
32 }
33 var colorMatch = COLOR_PATTERN.matcher(documentation);
34 if (colorMatch.find()) {
35 // Use a {@code _} instead of a {@code #} to signify hex codes, because the type hashes have to be valid
36 // CSS class names.
37 var color = colorMatch.group(1).toLowerCase(Locale.ROOT).replace("#", "_");
38 return Map.of(COLOR_TAG, color);
39 }
40 return Map.of();
41 }
42}
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 3080a78e..3dcf6b1f 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
@@ -18,6 +18,7 @@ import org.eclipse.xtext.resource.EObjectDescription;
18import org.eclipse.xtext.resource.IEObjectDescription; 18import org.eclipse.xtext.resource.IEObjectDescription;
19import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; 19import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy;
20import org.eclipse.xtext.util.IAcceptor; 20import org.eclipse.xtext.util.IAcceptor;
21import tools.refinery.language.documentation.DocumentationCommentParser;
21import tools.refinery.language.naming.ProblemQualifiedNameProvider; 22import tools.refinery.language.naming.ProblemQualifiedNameProvider;
22import tools.refinery.language.scoping.imports.ImportCollector; 23import tools.refinery.language.scoping.imports.ImportCollector;
23import tools.refinery.language.model.problem.*; 24import tools.refinery.language.model.problem.*;
@@ -56,6 +57,9 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti
56 @Inject 57 @Inject
57 private ImportCollector importCollector; 58 private ImportCollector importCollector;
58 59
60 @Inject
61 private DocumentationCommentParser documentationCommentParser;
62
59 @Override 63 @Override
60 public boolean createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) { 64 public boolean createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) {
61 if (!shouldExport(eObject)) { 65 if (!shouldExport(eObject)) {
@@ -154,6 +158,8 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti
154 if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { 158 if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) {
155 builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); 159 builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE);
156 } 160 }
161 var documentationMap = documentationCommentParser.parseDocumentation(eObject);
162 builder.putAll(documentationMap);
157 return builder.build(); 163 return builder.build();
158 } 164 }
159 165
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java
new file mode 100644
index 00000000..0566e7e2
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java
@@ -0,0 +1,94 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.documentation;
7
8import com.google.inject.Inject;
9import org.eclipse.xtext.testing.InjectWith;
10import org.eclipse.xtext.testing.extensions.InjectionExtension;
11import org.junit.jupiter.api.extension.ExtendWith;
12import org.junit.jupiter.params.ParameterizedTest;
13import org.junit.jupiter.params.provider.Arguments;
14import org.junit.jupiter.params.provider.MethodSource;
15import tools.refinery.language.documentation.DocumentationCommentParser;
16import tools.refinery.language.model.tests.utils.ProblemParseHelper;
17import tools.refinery.language.tests.ProblemInjectorProvider;
18
19import java.util.stream.Stream;
20
21import static org.hamcrest.MatcherAssert.assertThat;
22import static org.hamcrest.Matchers.is;
23
24@ExtendWith(InjectionExtension.class)
25@InjectWith(ProblemInjectorProvider.class)
26class DocumentationCommentParserTest {
27 @Inject
28 private ProblemParseHelper parseHelper;
29
30 @Inject
31 private DocumentationCommentParser commentParser;
32
33 @ParameterizedTest
34 @MethodSource
35 void colorTest(String text, String expectedColor) {
36 var parseResult = parseHelper.parse(text);
37 var foo = parseResult.findClass("Foo").classDeclaration();
38 var documentation = commentParser.parseDocumentation(foo);
39 var actualColor = documentation.get(DocumentationCommentParser.COLOR_TAG);
40 assertThat(actualColor, is(expectedColor));
41 }
42
43 static Stream<Arguments> colorTest() {
44 return Stream.of(
45 Arguments.of("class Foo.", null),
46 Arguments.of("""
47 % @color #ff0000
48 class Foo.
49 """, null),
50 Arguments.of("""
51 /*
52 * @color #ff0000
53 */
54 class Foo.
55 """, null),
56 Arguments.of("""
57 /**
58 * @color #ff0000
59 */
60 class Foo.
61 """, "_ff0000"),
62 Arguments.of("""
63 /**
64 * @color #ff0000 other
65 */
66 class Foo.
67 """, "_ff0000"),
68 Arguments.of("""
69 /** @color #ff0000 */
70 class Foo.
71 """, "_ff0000"),
72 Arguments.of("""
73 /**@color #ff0000*/
74 class Foo.
75 """, "_ff0000"),
76 Arguments.of("""
77 /**@color\t #ff0000*/
78 class Foo.
79 """, "_ff0000"),
80 Arguments.of("""
81 /** @color #F2af00 */
82 class Foo.
83 """, "_f2af00"),
84 Arguments.of("""
85 /** @color #Fa0 */
86 class Foo.
87 """, "_fa0"),
88 Arguments.of("""
89 /** @color 4 */
90 class Foo.
91 """, "4")
92 );
93 }
94}
diff --git a/yarn.lock b/yarn.lock
index 13125135..98cbf7fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2192,6 +2192,7 @@ __metadata:
2192 "@mui/system": "npm:^5.15.11" 2192 "@mui/system": "npm:^5.15.11"
2193 "@mui/x-data-grid": "npm:^6.19.5" 2193 "@mui/x-data-grid": "npm:^6.19.5"
2194 "@types/d3": "npm:^7.4.3" 2194 "@types/d3": "npm:^7.4.3"
2195 "@types/d3-color": "npm:^3.1.3"
2195 "@types/d3-graphviz": "npm:^2.6.10" 2196 "@types/d3-graphviz": "npm:^2.6.10"
2196 "@types/d3-selection": "npm:^3.0.10" 2197 "@types/d3-selection": "npm:^3.0.10"
2197 "@types/d3-zoom": "npm:^3.0.8" 2198 "@types/d3-zoom": "npm:^3.0.8"
@@ -2213,6 +2214,7 @@ __metadata:
2213 cross-env: "npm:^7.0.3" 2214 cross-env: "npm:^7.0.3"
2214 csstype: "npm:^3.1.3" 2215 csstype: "npm:^3.1.3"
2215 d3: "npm:^7.8.5" 2216 d3: "npm:^7.8.5"
2217 d3-color: "npm:^3.1.0"
2216 d3-graphviz: "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch" 2218 d3-graphviz: "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch"
2217 d3-selection: "npm:^3.0.0" 2219 d3-selection: "npm:^3.0.0"
2218 d3-zoom: "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch" 2220 d3-zoom: "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch"
@@ -2607,6 +2609,13 @@ __metadata:
2607 languageName: node 2609 languageName: node
2608 linkType: hard 2610 linkType: hard
2609 2611
2612"@types/d3-color@npm:^3.1.3":
2613 version: 3.1.3
2614 resolution: "@types/d3-color@npm:3.1.3"
2615 checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae
2616 languageName: node
2617 linkType: hard
2618
2610"@types/d3-contour@npm:*": 2619"@types/d3-contour@npm:*":
2611 version: 3.0.2 2620 version: 3.0.2
2612 resolution: "@types/d3-contour@npm:3.0.2" 2621 resolution: "@types/d3-contour@npm:3.0.2"
@@ -4133,7 +4142,7 @@ __metadata:
4133 languageName: node 4142 languageName: node
4134 linkType: hard 4143 linkType: hard
4135 4144
4136"d3-color@npm:1 - 3, d3-color@npm:3": 4145"d3-color@npm:1 - 3, d3-color@npm:3, d3-color@npm:^3.1.0":
4137 version: 3.1.0 4146 version: 3.1.0
4138 resolution: "d3-color@npm:3.1.0" 4147 resolution: "d3-color@npm:3.1.0"
4139 checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c 4148 checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c