aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2021-11-16 17:09:58 +0100
committerLibravatar GitHub <noreply@github.com>2021-11-16 17:09:58 +0100
commitb4ba1dc1fec50a8114d49879dcb30891b8a8107e (patch)
treec40e4af57988becdcf7d363a2fb7107f668b8ba0
parentchore(lang): fix Sonar issue (diff)
parentfeat(web): xtext formatter client (diff)
downloadrefinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.tar.gz
refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.tar.zst
refinery-b4ba1dc1fec50a8114d49879dcb30891b8a8107e.zip
Merge pull request #13 from kris7t/xtext-formatter
Xtext formatter support
-rw-r--r--language-web/package.json3
-rw-r--r--language-web/src/main/js/editor/EditorButtons.tsx9
-rw-r--r--language-web/src/main/js/editor/EditorStore.ts6
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts6
-rw-r--r--language-web/src/main/js/xtext/HighlightingService.ts12
-rw-r--r--language-web/src/main/js/xtext/OccurrencesService.ts31
-rw-r--r--language-web/src/main/js/xtext/UpdateService.ts125
-rw-r--r--language-web/src/main/js/xtext/ValidationService.ts11
-rw-r--r--language-web/src/main/js/xtext/XtextClient.ts13
-rw-r--r--language-web/src/main/js/xtext/XtextWebSocketClient.ts67
-rw-r--r--language-web/src/main/js/xtext/xtextMessages.ts78
-rw-r--r--language-web/src/main/js/xtext/xtextServiceResults.ts287
-rw-r--r--language-web/yarn.lock5
-rw-r--r--language/src/main/java/tools/refinery/language/GenerateProblem.mwe23
-rw-r--r--language/src/main/java/tools/refinery/language/Problem.xtext9
-rw-r--r--language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java183
-rw-r--r--language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java235
-rw-r--r--language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java42
18 files changed, 753 insertions, 372 deletions
diff --git a/language-web/package.json b/language-web/package.json
index 3362a47a..54aad155 100644
--- a/language-web/package.json
+++ b/language-web/package.json
@@ -95,6 +95,7 @@
95 "mobx-react-lite": "^3.2.1", 95 "mobx-react-lite": "^3.2.1",
96 "nanoid": "^3.1.30", 96 "nanoid": "^3.1.30",
97 "react": "^17.0.2", 97 "react": "^17.0.2",
98 "react-dom": "^17.0.2" 98 "react-dom": "^17.0.2",
99 "zod": "^3.11.6"
99 } 100 }
100} 101}
diff --git a/language-web/src/main/js/editor/EditorButtons.tsx b/language-web/src/main/js/editor/EditorButtons.tsx
index 09ce33dd..150aa00d 100644
--- a/language-web/src/main/js/editor/EditorButtons.tsx
+++ b/language-web/src/main/js/editor/EditorButtons.tsx
@@ -7,6 +7,7 @@ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
7import CheckIcon from '@mui/icons-material/Check'; 7import CheckIcon from '@mui/icons-material/Check';
8import ErrorIcon from '@mui/icons-material/Error'; 8import ErrorIcon from '@mui/icons-material/Error';
9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; 9import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
10import FormatPaint from '@mui/icons-material/FormatPaint';
10import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 11import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
11import RedoIcon from '@mui/icons-material/Redo'; 12import RedoIcon from '@mui/icons-material/Redo';
12import SearchIcon from '@mui/icons-material/Search'; 13import SearchIcon from '@mui/icons-material/Search';
@@ -47,7 +48,6 @@ export const EditorButtons = observer(() => {
47 disabled={!editorStore.canUndo} 48 disabled={!editorStore.canUndo}
48 onClick={() => editorStore.undo()} 49 onClick={() => editorStore.undo()}
49 aria-label="Undo" 50 aria-label="Undo"
50 value="undo"
51 > 51 >
52 <UndoIcon fontSize="small" /> 52 <UndoIcon fontSize="small" />
53 </IconButton> 53 </IconButton>
@@ -55,7 +55,6 @@ export const EditorButtons = observer(() => {
55 disabled={!editorStore.canRedo} 55 disabled={!editorStore.canRedo}
56 onClick={() => editorStore.redo()} 56 onClick={() => editorStore.redo()}
57 aria-label="Redo" 57 aria-label="Redo"
58 value="redo"
59 > 58 >
60 <RedoIcon fontSize="small" /> 59 <RedoIcon fontSize="small" />
61 </IconButton> 60 </IconButton>
@@ -88,6 +87,12 @@ export const EditorButtons = observer(() => {
88 {getLintIcon(editorStore.highestDiagnosticLevel)} 87 {getLintIcon(editorStore.highestDiagnosticLevel)}
89 </ToggleButton> 88 </ToggleButton>
90 </ToggleButtonGroup> 89 </ToggleButtonGroup>
90 <IconButton
91 onClick={() => editorStore.formatText()}
92 aria-label="Automatic format"
93 >
94 <FormatPaint fontSize="small" />
95 </IconButton>
91 </Stack> 96 </Stack>
92 ); 97 );
93}); 98});
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts
index 059233f4..5760de28 100644
--- a/language-web/src/main/js/editor/EditorStore.ts
+++ b/language-web/src/main/js/editor/EditorStore.ts
@@ -115,6 +115,7 @@ export class EditorStore {
115 lineNumbers(), 115 lineNumbers(),
116 foldGutter(), 116 foldGutter(),
117 keymap.of([ 117 keymap.of([
118 { key: 'Mod-Shift-f', run: () => this.formatText() },
118 ...closeBracketsKeymap, 119 ...closeBracketsKeymap,
119 ...commentKeymap, 120 ...commentKeymap,
120 ...completionKeymap, 121 ...completionKeymap,
@@ -280,4 +281,9 @@ export class EditorStore {
280 toggleLintPanel(): void { 281 toggleLintPanel(): void {
281 this.setLintPanelOpen(!this.showLintPanel); 282 this.setLintPanelOpen(!this.showLintPanel);
282 } 283 }
284
285 formatText(): boolean {
286 this.client.formatText();
287 return true;
288 }
283} 289}
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
index aa9a80b0..8b872e06 100644
--- a/language-web/src/main/js/xtext/ContentAssistService.ts
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -10,7 +10,7 @@ import escapeStringRegexp from 'escape-string-regexp';
10import { implicitCompletion } from '../language/props'; 10import { implicitCompletion } from '../language/props';
11import type { UpdateService } from './UpdateService'; 11import type { UpdateService } from './UpdateService';
12import { getLogger } from '../utils/logger'; 12import { getLogger } from '../utils/logger';
13import type { IContentAssistEntry } from './xtextServiceResults'; 13import type { ContentAssistEntry } from './xtextServiceResults';
14 14
15const PROPOSALS_LIMIT = 1000; 15const PROPOSALS_LIMIT = 1000;
16 16
@@ -67,8 +67,8 @@ function computeSpan(prefix: string, entryCount: number): RegExp {
67 return new RegExp(`^${escapedPrefix}$`); 67 return new RegExp(`^${escapedPrefix}$`);
68} 68}
69 69
70function createCompletion(entry: IContentAssistEntry): Completion { 70function createCompletion(entry: ContentAssistEntry): Completion {
71 let boost; 71 let boost: number;
72 switch (entry.kind) { 72 switch (entry.kind) {
73 case 'KEYWORD': 73 case 'KEYWORD':
74 // Some hard-to-type operators should be on top. 74 // Some hard-to-type operators should be on top.
diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts
index fc3e9e53..dfbb4a19 100644
--- a/language-web/src/main/js/xtext/HighlightingService.ts
+++ b/language-web/src/main/js/xtext/HighlightingService.ts
@@ -1,10 +1,7 @@
1import type { EditorStore } from '../editor/EditorStore'; 1import type { EditorStore } from '../editor/EditorStore';
2import type { IHighlightRange } from '../editor/semanticHighlighting'; 2import type { IHighlightRange } from '../editor/semanticHighlighting';
3import type { UpdateService } from './UpdateService'; 3import type { UpdateService } from './UpdateService';
4import { getLogger } from '../utils/logger'; 4import { highlightingResult } from './xtextServiceResults';
5import { isHighlightingResult } from './xtextServiceResults';
6
7const log = getLogger('xtext.ValidationService');
8 5
9export class HighlightingService { 6export class HighlightingService {
10 private readonly store: EditorStore; 7 private readonly store: EditorStore;
@@ -17,13 +14,10 @@ export class HighlightingService {
17 } 14 }
18 15
19 onPush(push: unknown): void { 16 onPush(push: unknown): void {
20 if (!isHighlightingResult(push)) { 17 const { regions } = highlightingResult.parse(push);
21 log.error('Invalid highlighting result', push);
22 return;
23 }
24 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 18 const allChanges = this.updateService.computeChangesSinceLastUpdate();
25 const ranges: IHighlightRange[] = []; 19 const ranges: IHighlightRange[] = [];
26 push.regions.forEach(({ offset, length, styleClasses }) => { 20 regions.forEach(({ offset, length, styleClasses }) => {
27 if (styleClasses.length === 0) { 21 if (styleClasses.length === 0) {
28 return; 22 return;
29 } 23 }
diff --git a/language-web/src/main/js/xtext/OccurrencesService.ts b/language-web/src/main/js/xtext/OccurrencesService.ts
index d1dec9e9..bc865537 100644
--- a/language-web/src/main/js/xtext/OccurrencesService.ts
+++ b/language-web/src/main/js/xtext/OccurrencesService.ts
@@ -7,9 +7,9 @@ import { getLogger } from '../utils/logger';
7import { Timer } from '../utils/Timer'; 7import { Timer } from '../utils/Timer';
8import { XtextWebSocketClient } from './XtextWebSocketClient'; 8import { XtextWebSocketClient } from './XtextWebSocketClient';
9import { 9import {
10 isOccurrencesResult, 10 isConflictResult,
11 isServiceConflictResult, 11 occurrencesResult,
12 ITextRegion, 12 TextRegion,
13} from './xtextServiceResults'; 13} from './xtextServiceResults';
14 14
15const FIND_OCCURRENCES_TIMEOUT_MS = 1000; 15const FIND_OCCURRENCES_TIMEOUT_MS = 1000;
@@ -20,7 +20,7 @@ const CLEAR_OCCURRENCES_TIMEOUT_MS = 10;
20 20
21const log = getLogger('xtext.OccurrencesService'); 21const log = getLogger('xtext.OccurrencesService');
22 22
23function transformOccurrences(regions: ITextRegion[]): IOccurrence[] { 23function transformOccurrences(regions: TextRegion[]): IOccurrence[] {
24 const occurrences: IOccurrence[] = []; 24 const occurrences: IOccurrence[] = [];
25 regions.forEach(({ offset, length }) => { 25 regions.forEach(({ offset, length }) => {
26 if (length > 0) { 26 if (length > 0) {
@@ -87,21 +87,32 @@ export class OccurrencesService {
87 caretOffset: this.store.state.selection.main.head, 87 caretOffset: this.store.state.selection.main.head,
88 }); 88 });
89 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 89 const allChanges = this.updateService.computeChangesSinceLastUpdate();
90 if (!allChanges.empty 90 if (!allChanges.empty || isConflictResult(result, 'canceled')) {
91 || (isServiceConflictResult(result) && result.conflict === 'canceled')) {
92 // Stale occurrences result, the user already made some changes. 91 // Stale occurrences result, the user already made some changes.
93 // We can safely ignore the occurrences and schedule a new find occurrences call. 92 // We can safely ignore the occurrences and schedule a new find occurrences call.
94 this.clearOccurrences(); 93 this.clearOccurrences();
95 this.findOccurrencesTimer.schedule(); 94 this.findOccurrencesTimer.schedule();
96 return; 95 return;
97 } 96 }
98 if (!isOccurrencesResult(result) || result.stateId !== this.updateService.xtextStateId) { 97 const parsedOccurrencesResult = occurrencesResult.safeParse(result);
99 log.error('Unexpected occurrences result', result); 98 if (!parsedOccurrencesResult.success) {
99 log.error(
100 'Unexpected occurences result',
101 result,
102 'not an OccurrencesResult: ',
103 parsedOccurrencesResult.error,
104 );
100 this.clearOccurrences(); 105 this.clearOccurrences();
101 return; 106 return;
102 } 107 }
103 const write = transformOccurrences(result.writeRegions); 108 const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data;
104 const read = transformOccurrences(result.readRegions); 109 if (stateId !== this.updateService.xtextStateId) {
110 log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId);
111 this.clearOccurrences();
112 return;
113 }
114 const write = transformOccurrences(writeRegions);
115 const read = transformOccurrences(readRegions);
105 this.hasOccurrences = write.length > 0 || read.length > 0; 116 this.hasOccurrences = write.length > 0 || read.length > 0;
106 log.debug('Found', write.length, 'write and', read.length, 'read occurrences'); 117 log.debug('Found', write.length, 'write and', read.length, 'read occurrences');
107 this.store.updateOccurrences(write, read); 118 this.store.updateOccurrences(write, read);
diff --git a/language-web/src/main/js/xtext/UpdateService.ts b/language-web/src/main/js/xtext/UpdateService.ts
index 9b672e79..e78944a9 100644
--- a/language-web/src/main/js/xtext/UpdateService.ts
+++ b/language-web/src/main/js/xtext/UpdateService.ts
@@ -1,6 +1,8 @@
1import { 1import {
2 ChangeDesc, 2 ChangeDesc,
3 ChangeSet, 3 ChangeSet,
4 ChangeSpec,
5 StateEffect,
4 Transaction, 6 Transaction,
5} from '@codemirror/state'; 7} from '@codemirror/state';
6import { nanoid } from 'nanoid'; 8import { nanoid } from 'nanoid';
@@ -11,10 +13,11 @@ import { ConditionVariable } from '../utils/ConditionVariable';
11import { getLogger } from '../utils/logger'; 13import { getLogger } from '../utils/logger';
12import { Timer } from '../utils/Timer'; 14import { Timer } from '../utils/Timer';
13import { 15import {
14 IContentAssistEntry, 16 ContentAssistEntry,
15 isContentAssistResult, 17 contentAssistResult,
16 isDocumentStateResult, 18 documentStateResult,
17 isInvalidStateIdConflictResult, 19 formattingResult,
20 isConflictResult,
18} from './xtextServiceResults'; 21} from './xtextServiceResults';
19 22
20const UPDATE_TIMEOUT_MS = 500; 23const UPDATE_TIMEOUT_MS = 500;
@@ -23,6 +26,8 @@ const WAIT_FOR_UPDATE_TIMEOUT_MS = 1000;
23 26
24const log = getLogger('xtext.UpdateService'); 27const log = getLogger('xtext.UpdateService');
25 28
29const setDirtyChanges = StateEffect.define<ChangeSet>();
30
26export interface IAbortSignal { 31export interface IAbortSignal {
27 aborted: boolean; 32 aborted: boolean;
28} 33}
@@ -38,12 +43,12 @@ export class UpdateService {
38 * The changes being synchronized to the server if a full or delta text update is running, 43 * The changes being synchronized to the server if a full or delta text update is running,
39 * `null` otherwise. 44 * `null` otherwise.
40 */ 45 */
41 private pendingUpdate: ChangeDesc | null = null; 46 private pendingUpdate: ChangeSet | null = null;
42 47
43 /** 48 /**
44 * Local changes not yet sychronized to the server and not part of the running update, if any. 49 * Local changes not yet sychronized to the server and not part of the running update, if any.
45 */ 50 */
46 private dirtyChanges: ChangeDesc; 51 private dirtyChanges: ChangeSet;
47 52
48 private readonly webSocketClient: XtextWebSocketClient; 53 private readonly webSocketClient: XtextWebSocketClient;
49 54
@@ -59,7 +64,7 @@ export class UpdateService {
59 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) { 64 constructor(store: EditorStore, webSocketClient: XtextWebSocketClient) {
60 this.resourceName = `${nanoid(7)}.problem`; 65 this.resourceName = `${nanoid(7)}.problem`;
61 this.store = store; 66 this.store = store;
62 this.dirtyChanges = this.newEmptyChangeDesc(); 67 this.dirtyChanges = this.newEmptyChangeSet();
63 this.webSocketClient = webSocketClient; 68 this.webSocketClient = webSocketClient;
64 } 69 }
65 70
@@ -71,8 +76,19 @@ export class UpdateService {
71 } 76 }
72 77
73 onTransaction(transaction: Transaction): void { 78 onTransaction(transaction: Transaction): void {
79 const setDirtyChangesEffect = transaction.effects.find(
80 (effect) => effect.is(setDirtyChanges),
81 ) as StateEffect<ChangeSet> | undefined;
82 if (setDirtyChangesEffect) {
83 const { value } = setDirtyChangesEffect;
84 if (this.pendingUpdate !== null) {
85 this.pendingUpdate = ChangeSet.empty(value.length);
86 }
87 this.dirtyChanges = value;
88 return;
89 }
74 if (transaction.docChanged) { 90 if (transaction.docChanged) {
75 this.dirtyChanges = this.dirtyChanges.composeDesc(transaction.changes.desc); 91 this.dirtyChanges = this.dirtyChanges.compose(transaction.changes);
76 this.idleUpdateTimer.reschedule(); 92 this.idleUpdateTimer.reschedule();
77 } 93 }
78 } 94 }
@@ -86,7 +102,7 @@ export class UpdateService {
86 * @return the summary of changes since the last update 102 * @return the summary of changes since the last update
87 */ 103 */
88 computeChangesSinceLastUpdate(): ChangeDesc { 104 computeChangesSinceLastUpdate(): ChangeDesc {
89 return this.pendingUpdate?.composeDesc(this.dirtyChanges) || this.dirtyChanges; 105 return this.pendingUpdate?.composeDesc(this.dirtyChanges.desc) || this.dirtyChanges.desc;
90 } 106 }
91 107
92 private handleIdleUpdate() { 108 private handleIdleUpdate() {
@@ -101,9 +117,8 @@ export class UpdateService {
101 this.idleUpdateTimer.reschedule(); 117 this.idleUpdateTimer.reschedule();
102 } 118 }
103 119
104 private newEmptyChangeDesc() { 120 private newEmptyChangeSet() {
105 const changeSet = ChangeSet.of([], this.store.state.doc.length); 121 return ChangeSet.of([], this.store.state.doc.length);
106 return changeSet.desc;
107 } 122 }
108 123
109 async updateFullText(): Promise<void> { 124 async updateFullText(): Promise<void> {
@@ -116,11 +131,8 @@ export class UpdateService {
116 serviceType: 'update', 131 serviceType: 'update',
117 fullText: this.store.state.doc.sliceString(0), 132 fullText: this.store.state.doc.sliceString(0),
118 }); 133 });
119 if (isDocumentStateResult(result)) { 134 const { stateId } = documentStateResult.parse(result);
120 return [result.stateId, undefined]; 135 return [stateId, undefined];
121 }
122 log.error('Unexpected full text update result:', result);
123 throw new Error('Full text update failed');
124 } 136 }
125 137
126 /** 138 /**
@@ -146,14 +158,14 @@ export class UpdateService {
146 requiredStateId: this.xtextStateId, 158 requiredStateId: this.xtextStateId,
147 ...delta, 159 ...delta,
148 }); 160 });
149 if (isDocumentStateResult(result)) { 161 const parsedDocumentStateResult = documentStateResult.safeParse(result);
150 return [result.stateId, undefined]; 162 if (parsedDocumentStateResult.success) {
163 return [parsedDocumentStateResult.data.stateId, undefined];
151 } 164 }
152 if (isInvalidStateIdConflictResult(result)) { 165 if (isConflictResult(result, 'invalidStateId')) {
153 return this.doFallbackToUpdateFullText(); 166 return this.doFallbackToUpdateFullText();
154 } 167 }
155 log.error('Unexpected delta text update result:', result); 168 throw parsedDocumentStateResult.error;
156 throw new Error('Delta text update failed');
157 }); 169 });
158 } 170 }
159 171
@@ -163,15 +175,15 @@ export class UpdateService {
163 } 175 }
164 log.warn('Delta update failed, performing full text update'); 176 log.warn('Delta update failed, performing full text update');
165 this.xtextStateId = null; 177 this.xtextStateId = null;
166 this.pendingUpdate = this.pendingUpdate.composeDesc(this.dirtyChanges); 178 this.pendingUpdate = this.pendingUpdate.compose(this.dirtyChanges);
167 this.dirtyChanges = this.newEmptyChangeDesc(); 179 this.dirtyChanges = this.newEmptyChangeSet();
168 return this.doUpdateFullText(); 180 return this.doUpdateFullText();
169 } 181 }
170 182
171 async fetchContentAssist( 183 async fetchContentAssist(
172 params: Record<string, unknown>, 184 params: Record<string, unknown>,
173 signal: IAbortSignal, 185 signal: IAbortSignal,
174 ): Promise<IContentAssistEntry[]> { 186 ): Promise<ContentAssistEntry[]> {
175 await this.prepareForDeltaUpdate(); 187 await this.prepareForDeltaUpdate();
176 if (signal.aborted) { 188 if (signal.aborted) {
177 return []; 189 return [];
@@ -185,18 +197,20 @@ export class UpdateService {
185 requiredStateId: this.xtextStateId, 197 requiredStateId: this.xtextStateId,
186 ...delta, 198 ...delta,
187 }); 199 });
188 if (isContentAssistResult(result)) { 200 const parsedContentAssistResult = contentAssistResult.safeParse(result);
189 return [result.stateId, result.entries]; 201 if (parsedContentAssistResult.success) {
202 const { stateId, entries: resultEntries } = parsedContentAssistResult.data;
203 return [stateId, resultEntries];
190 } 204 }
191 if (isInvalidStateIdConflictResult(result)) { 205 if (isConflictResult(result, 'invalidStateId')) {
206 log.warn('Server state invalid during content assist');
192 const [newStateId] = await this.doFallbackToUpdateFullText(); 207 const [newStateId] = await this.doFallbackToUpdateFullText();
193 // We must finish this state update transaction to prepare for any push events 208 // We must finish this state update transaction to prepare for any push events
194 // before querying for content assist, so we just return `null` and will query 209 // before querying for content assist, so we just return `null` and will query
195 // the content assist service later. 210 // the content assist service later.
196 return [newStateId, null]; 211 return [newStateId, null];
197 } 212 }
198 log.error('Unextpected content assist result with delta update', result); 213 throw parsedContentAssistResult.error;
199 throw new Error('Unexpexted content assist result with delta update');
200 }); 214 });
201 if (entries !== null) { 215 if (entries !== null) {
202 return entries; 216 return entries;
@@ -214,11 +228,36 @@ export class UpdateService {
214 ...params, 228 ...params,
215 requiredStateId: expectedStateId, 229 requiredStateId: expectedStateId,
216 }); 230 });
217 if (isContentAssistResult(result) && result.stateId === expectedStateId) { 231 const { stateId, entries } = contentAssistResult.parse(result);
218 return result.entries; 232 if (stateId !== expectedStateId) {
233 throw new Error(`Unexpected state id, expected: ${expectedStateId} got: ${stateId}`);
219 } 234 }
220 log.error('Unexpected content assist result', result); 235 return entries;
221 throw new Error('Unexpected content assist result'); 236 }
237
238 async formatText(): Promise<void> {
239 await this.update();
240 let { from, to } = this.store.state.selection.main;
241 if (to <= from) {
242 from = 0;
243 to = this.store.state.doc.length;
244 }
245 log.debug('Formatting from', from, 'to', to);
246 await this.withUpdate(async () => {
247 const result = await this.webSocketClient.send({
248 resource: this.resourceName,
249 serviceType: 'format',
250 selectionStart: from,
251 selectionEnd: to,
252 });
253 const { stateId, formattedText } = formattingResult.parse(result);
254 this.applyBeforeDirtyChanges({
255 from,
256 to,
257 insert: formattedText,
258 });
259 return [stateId, null];
260 });
222 } 261 }
223 262
224 private computeDelta() { 263 private computeDelta() {
@@ -242,6 +281,20 @@ export class UpdateService {
242 }; 281 };
243 } 282 }
244 283
284 private applyBeforeDirtyChanges(changeSpec: ChangeSpec) {
285 const pendingChanges = this.pendingUpdate?.compose(this.dirtyChanges) || this.dirtyChanges;
286 const revertChanges = pendingChanges.invert(this.store.state.doc);
287 const applyBefore = ChangeSet.of(changeSpec, revertChanges.newLength);
288 const redoChanges = pendingChanges.map(applyBefore.desc);
289 const changeSet = revertChanges.compose(applyBefore).compose(redoChanges);
290 this.store.dispatch({
291 changes: changeSet,
292 effects: [
293 setDirtyChanges.of(redoChanges),
294 ],
295 });
296 }
297
245 /** 298 /**
246 * Executes an asynchronous callback that updates the state on the server. 299 * Executes an asynchronous callback that updates the state on the server.
247 * 300 *
@@ -268,7 +321,7 @@ export class UpdateService {
268 throw new Error('Another update is pending, will not perform update'); 321 throw new Error('Another update is pending, will not perform update');
269 } 322 }
270 this.pendingUpdate = this.dirtyChanges; 323 this.pendingUpdate = this.dirtyChanges;
271 this.dirtyChanges = this.newEmptyChangeDesc(); 324 this.dirtyChanges = this.newEmptyChangeSet();
272 let newStateId: string | null = null; 325 let newStateId: string | null = null;
273 try { 326 try {
274 let result: T; 327 let result: T;
@@ -282,7 +335,7 @@ export class UpdateService {
282 if (this.pendingUpdate === null) { 335 if (this.pendingUpdate === null) {
283 log.error('pendingUpdate was cleared during update'); 336 log.error('pendingUpdate was cleared during update');
284 } else { 337 } else {
285 this.dirtyChanges = this.pendingUpdate.composeDesc(this.dirtyChanges); 338 this.dirtyChanges = this.pendingUpdate.compose(this.dirtyChanges);
286 } 339 }
287 this.pendingUpdate = null; 340 this.pendingUpdate = null;
288 this.webSocketClient.forceReconnectOnError(); 341 this.webSocketClient.forceReconnectOnError();
diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts
index 8e4934ac..c7f6ac7f 100644
--- a/language-web/src/main/js/xtext/ValidationService.ts
+++ b/language-web/src/main/js/xtext/ValidationService.ts
@@ -3,9 +3,7 @@ import type { Diagnostic } from '@codemirror/lint';
3import type { EditorStore } from '../editor/EditorStore'; 3import type { EditorStore } from '../editor/EditorStore';
4import type { UpdateService } from './UpdateService'; 4import type { UpdateService } from './UpdateService';
5import { getLogger } from '../utils/logger'; 5import { getLogger } from '../utils/logger';
6import { isValidationResult } from './xtextServiceResults'; 6import { validationResult } from './xtextServiceResults';
7
8const log = getLogger('xtext.ValidationService');
9 7
10export class ValidationService { 8export class ValidationService {
11 private readonly store: EditorStore; 9 private readonly store: EditorStore;
@@ -18,13 +16,10 @@ export class ValidationService {
18 } 16 }
19 17
20 onPush(push: unknown): void { 18 onPush(push: unknown): void {
21 if (!isValidationResult(push)) { 19 const { issues } = validationResult.parse(push);
22 log.error('Invalid validation result', push);
23 return;
24 }
25 const allChanges = this.updateService.computeChangesSinceLastUpdate(); 20 const allChanges = this.updateService.computeChangesSinceLastUpdate();
26 const diagnostics: Diagnostic[] = []; 21 const diagnostics: Diagnostic[] = [];
27 push.issues.forEach(({ 22 issues.forEach(({
28 offset, 23 offset,
29 length, 24 length,
30 severity, 25 severity,
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts
index 28f3d0cc..0898e725 100644
--- a/language-web/src/main/js/xtext/XtextClient.ts
+++ b/language-web/src/main/js/xtext/XtextClient.ts
@@ -12,6 +12,7 @@ import { UpdateService } from './UpdateService';
12import { getLogger } from '../utils/logger'; 12import { getLogger } from '../utils/logger';
13import { ValidationService } from './ValidationService'; 13import { ValidationService } from './ValidationService';
14import { XtextWebSocketClient } from './XtextWebSocketClient'; 14import { XtextWebSocketClient } from './XtextWebSocketClient';
15import { XtextWebPushService } from './xtextMessages';
15 16
16const log = getLogger('xtext.XtextClient'); 17const log = getLogger('xtext.XtextClient');
17 18
@@ -52,7 +53,7 @@ export class XtextClient {
52 this.occurrencesService.onTransaction(transaction); 53 this.occurrencesService.onTransaction(transaction);
53 } 54 }
54 55
55 private onPush(resource: string, stateId: string, service: string, push: unknown) { 56 private onPush(resource: string, stateId: string, service: XtextWebPushService, push: unknown) {
56 const { resourceName, xtextStateId } = this.updateService; 57 const { resourceName, xtextStateId } = this.updateService;
57 if (resource !== resourceName) { 58 if (resource !== resourceName) {
58 log.error('Unknown resource name: expected:', resourceName, 'got:', resource); 59 log.error('Unknown resource name: expected:', resourceName, 'got:', resource);
@@ -70,14 +71,16 @@ export class XtextClient {
70 return; 71 return;
71 case 'validate': 72 case 'validate':
72 this.validationService.onPush(push); 73 this.validationService.onPush(push);
73 return;
74 default:
75 log.error('Unknown push service:', service);
76 break;
77 } 74 }
78 } 75 }
79 76
80 contentAssist(context: CompletionContext): Promise<CompletionResult> { 77 contentAssist(context: CompletionContext): Promise<CompletionResult> {
81 return this.contentAssistService.contentAssist(context); 78 return this.contentAssistService.contentAssist(context);
82 } 79 }
80
81 formatText(): void {
82 this.updateService.formatText().catch((e) => {
83 log.error('Error while formatting text', e);
84 });
85 }
83} 86}
diff --git a/language-web/src/main/js/xtext/XtextWebSocketClient.ts b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
index 488e4b3b..2ce20a54 100644
--- a/language-web/src/main/js/xtext/XtextWebSocketClient.ts
+++ b/language-web/src/main/js/xtext/XtextWebSocketClient.ts
@@ -4,12 +4,13 @@ import { getLogger } from '../utils/logger';
4import { PendingTask } from '../utils/PendingTask'; 4import { PendingTask } from '../utils/PendingTask';
5import { Timer } from '../utils/Timer'; 5import { Timer } from '../utils/Timer';
6import { 6import {
7 isErrorResponse, 7 xtextWebErrorResponse,
8 isOkResponse, 8 XtextWebRequest,
9 isPushMessage, 9 xtextWebOkResponse,
10 IXtextWebRequest, 10 xtextWebPushMessage,
11 XtextWebPushService,
11} from './xtextMessages'; 12} from './xtextMessages';
12import { isPongResult } from './xtextServiceResults'; 13import { pongResult } from './xtextServiceResults';
13 14
14const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1'; 15const XTEXT_SUBPROTOCOL_V1 = 'tools.refinery.language.web.xtext.v1';
15 16
@@ -32,7 +33,7 @@ export type ReconnectHandler = () => void;
32export type PushHandler = ( 33export type PushHandler = (
33 resourceId: string, 34 resourceId: string,
34 stateId: string, 35 stateId: string,
35 service: string, 36 service: XtextWebPushService,
36 data: unknown, 37 data: unknown,
37) => void; 38) => void;
38 39
@@ -192,11 +193,12 @@ export class XtextWebSocketClient {
192 const ping = nanoid(); 193 const ping = nanoid();
193 log.trace('Ping', ping); 194 log.trace('Ping', ping);
194 this.send({ ping }).then((result) => { 195 this.send({ ping }).then((result) => {
195 if (isPongResult(result) && result.pong === ping) { 196 const parsedPongResult = pongResult.safeParse(result);
197 if (parsedPongResult.success && parsedPongResult.data.pong === ping) {
196 log.trace('Pong', ping); 198 log.trace('Pong', ping);
197 this.pingTimer.schedule(); 199 this.pingTimer.schedule();
198 } else { 200 } else {
199 log.error('Invalid pong'); 201 log.error('Invalid pong:', parsedPongResult, 'expected:', ping);
200 this.forceReconnectOnError(); 202 this.forceReconnectOnError();
201 } 203 }
202 }).catch((error) => { 204 }).catch((error) => {
@@ -222,7 +224,7 @@ export class XtextWebSocketClient {
222 const message = JSON.stringify({ 224 const message = JSON.stringify({
223 id: messageId, 225 id: messageId,
224 request, 226 request,
225 } as IXtextWebRequest); 227 } as XtextWebRequest);
226 log.trace('Sending message', message); 228 log.trace('Sending message', message);
227 return new Promise((resolve, reject) => { 229 return new Promise((resolve, reject) => {
228 const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => { 230 const task = new PendingTask(resolve, reject, REQUEST_TIMEOUT_MS, () => {
@@ -248,23 +250,42 @@ export class XtextWebSocketClient {
248 this.forceReconnectOnError(); 250 this.forceReconnectOnError();
249 return; 251 return;
250 } 252 }
251 if (isOkResponse(message)) { 253 const okResponse = xtextWebOkResponse.safeParse(message);
252 this.resolveRequest(message.id, message.response); 254 if (okResponse.success) {
253 } else if (isErrorResponse(message)) { 255 const { id, response } = okResponse.data;
254 this.rejectRequest(message.id, new Error(`${message.error} error: ${message.message}`)); 256 this.resolveRequest(id, response);
255 if (message.error === 'server') { 257 return;
256 log.error('Reconnecting due to server error: ', message.message); 258 }
259 const errorResponse = xtextWebErrorResponse.safeParse(message);
260 if (errorResponse.success) {
261 const { id, error, message: errorMessage } = errorResponse.data;
262 this.rejectRequest(id, new Error(`${error} error: ${errorMessage}`));
263 if (error === 'server') {
264 log.error('Reconnecting due to server error: ', errorMessage);
257 this.forceReconnectOnError(); 265 this.forceReconnectOnError();
258 } 266 }
259 } else if (isPushMessage(message)) { 267 return;
260 this.onPush( 268 }
261 message.resource, 269 const pushMessage = xtextWebPushMessage.safeParse(message);
262 message.stateId, 270 if (pushMessage.success) {
263 message.service, 271 const {
264 message.push, 272 resource,
265 ); 273 stateId,
274 service,
275 push,
276 } = pushMessage.data;
277 this.onPush(resource, stateId, service, push);
266 } else { 278 } else {
267 log.error('Unexpected websocket message', message); 279 log.error(
280 'Unexpected websocket message:',
281 message,
282 'not ok response because:',
283 okResponse.error,
284 'not error response because:',
285 errorResponse.error,
286 'not push message because:',
287 pushMessage.error,
288 );
268 this.forceReconnectOnError(); 289 this.forceReconnectOnError();
269 } 290 }
270 } 291 }
diff --git a/language-web/src/main/js/xtext/xtextMessages.ts b/language-web/src/main/js/xtext/xtextMessages.ts
index 68737958..c4305fcf 100644
--- a/language-web/src/main/js/xtext/xtextMessages.ts
+++ b/language-web/src/main/js/xtext/xtextMessages.ts
@@ -1,62 +1,40 @@
1export interface IXtextWebRequest { 1import { z } from 'zod';
2 id: string;
3 2
4 request: unknown; 3export const xtextWebRequest = z.object({
5} 4 id: z.string().nonempty(),
5 request: z.unknown(),
6});
6 7
7export interface IXtextWebOkResponse { 8export type XtextWebRequest = z.infer<typeof xtextWebRequest>;
8 id: string;
9 9
10 response: unknown; 10export const xtextWebOkResponse = z.object({
11} 11 id: z.string().nonempty(),
12 response: z.unknown(),
13});
12 14
13export function isOkResponse(response: unknown): response is IXtextWebOkResponse { 15export type XtextWebOkResponse = z.infer<typeof xtextWebOkResponse>;
14 const okResponse = response as IXtextWebOkResponse;
15 return typeof okResponse === 'object'
16 && typeof okResponse.id === 'string'
17 && typeof okResponse.response !== 'undefined';
18}
19 16
20export const VALID_XTEXT_WEB_ERROR_KINDS = ['request', 'server'] as const; 17export const xtextWebErrorKind = z.enum(['request', 'server']);
21 18
22export type XtextWebErrorKind = typeof VALID_XTEXT_WEB_ERROR_KINDS[number]; 19export type XtextWebErrorKind = z.infer<typeof xtextWebErrorKind>;
23 20
24export function isXtextWebErrorKind(value: unknown): value is XtextWebErrorKind { 21export const xtextWebErrorResponse = z.object({
25 return typeof value === 'string' 22 id: z.string().nonempty(),
26 && VALID_XTEXT_WEB_ERROR_KINDS.includes(value as XtextWebErrorKind); 23 error: xtextWebErrorKind,
27} 24 message: z.string(),
25});
28 26
29export interface IXtextWebErrorResponse { 27export type XtextWebErrorResponse = z.infer<typeof xtextWebErrorResponse>;
30 id: string;
31 28
32 error: XtextWebErrorKind; 29export const xtextWebPushService = z.enum(['highlight', 'validate']);
33 30
34 message: string; 31export type XtextWebPushService = z.infer<typeof xtextWebPushService>;
35}
36 32
37export function isErrorResponse(response: unknown): response is IXtextWebErrorResponse { 33export const xtextWebPushMessage = z.object({
38 const errorResponse = response as IXtextWebErrorResponse; 34 resource: z.string().nonempty(),
39 return typeof errorResponse === 'object' 35 stateId: z.string().nonempty(),
40 && typeof errorResponse.id === 'string' 36 service: xtextWebPushService,
41 && isXtextWebErrorKind(errorResponse.error) 37 push: z.unknown(),
42 && typeof errorResponse.message === 'string'; 38});
43}
44 39
45export interface IXtextWebPushMessage { 40export type XtextWebPushMessage = z.infer<typeof xtextWebPushMessage>;
46 resource: string;
47
48 stateId: string;
49
50 service: string;
51
52 push: unknown;
53}
54
55export function isPushMessage(response: unknown): response is IXtextWebPushMessage {
56 const pushMessage = response as IXtextWebPushMessage;
57 return typeof pushMessage === 'object'
58 && typeof pushMessage.resource === 'string'
59 && typeof pushMessage.stateId === 'string'
60 && typeof pushMessage.service === 'string'
61 && typeof pushMessage.push !== 'undefined';
62}
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts
index b2de1e4a..f79b059c 100644
--- a/language-web/src/main/js/xtext/xtextServiceResults.ts
+++ b/language-web/src/main/js/xtext/xtextServiceResults.ts
@@ -1,239 +1,112 @@
1export interface IPongResult { 1import { z } from 'zod';
2 pong: string;
3}
4
5export function isPongResult(result: unknown): result is IPongResult {
6 const pongResult = result as IPongResult;
7 return typeof pongResult === 'object'
8 && typeof pongResult.pong === 'string';
9}
10
11export interface IDocumentStateResult {
12 stateId: string;
13}
14
15export function isDocumentStateResult(result: unknown): result is IDocumentStateResult {
16 const documentStateResult = result as IDocumentStateResult;
17 return typeof documentStateResult === 'object'
18 && typeof documentStateResult.stateId === 'string';
19}
20
21export const VALID_CONFLICTS = ['invalidStateId', 'canceled'] as const;
22
23export type Conflict = typeof VALID_CONFLICTS[number];
24
25export function isConflict(value: unknown): value is Conflict {
26 return typeof value === 'string' && VALID_CONFLICTS.includes(value as Conflict);
27}
28
29export interface IServiceConflictResult {
30 conflict: Conflict;
31}
32
33export function isServiceConflictResult(result: unknown): result is IServiceConflictResult {
34 const serviceConflictResult = result as IServiceConflictResult;
35 return typeof serviceConflictResult === 'object'
36 && isConflict(serviceConflictResult.conflict);
37}
38
39export function isInvalidStateIdConflictResult(result: unknown): boolean {
40 return isServiceConflictResult(result) && result.conflict === 'invalidStateId';
41}
42
43export const VALID_SEVERITIES = ['error', 'warning', 'info', 'ignore'] as const;
44
45export type Severity = typeof VALID_SEVERITIES[number];
46
47export function isSeverity(value: unknown): value is Severity {
48 return typeof value === 'string' && VALID_SEVERITIES.includes(value as Severity);
49}
50
51export interface IIssue {
52 description: string;
53
54 severity: Severity;
55
56 line: number;
57
58 column: number;
59
60 offset: number;
61 2
62 length: number; 3export const pongResult = z.object({
63} 4 pong: z.string().nonempty(),
5});
64 6
65export function isIssue(value: unknown): value is IIssue { 7export type PongResult = z.infer<typeof pongResult>;
66 const issue = value as IIssue;
67 return typeof issue === 'object'
68 && typeof issue.description === 'string'
69 && isSeverity(issue.severity)
70 && typeof issue.line === 'number'
71 && typeof issue.column === 'number'
72 && typeof issue.offset === 'number'
73 && typeof issue.length === 'number';
74}
75 8
76export interface IValidationResult { 9export const documentStateResult = z.object({
77 issues: IIssue[]; 10 stateId: z.string().nonempty(),
78} 11});
79 12
80function isArrayOfType<T>(value: unknown, check: (entry: unknown) => entry is T): value is T[] { 13export type DocumentStateResult = z.infer<typeof documentStateResult>;
81 return Array.isArray(value) && (value as T[]).every(check);
82}
83 14
84export function isValidationResult(result: unknown): result is IValidationResult { 15export const conflict = z.enum(['invalidStateId', 'canceled']);
85 const validationResult = result as IValidationResult;
86 return typeof validationResult === 'object'
87 && isArrayOfType(validationResult.issues, isIssue);
88}
89
90export interface IReplaceRegion {
91 offset: number;
92 16
93 length: number; 17export type Conflict = z.infer<typeof conflict>;
94 18
95 text: string; 19export const serviceConflictResult = z.object({
96} 20 conflict,
21});
97 22
98export function isReplaceRegion(value: unknown): value is IReplaceRegion { 23export type ServiceConflictResult = z.infer<typeof serviceConflictResult>;
99 const replaceRegion = value as IReplaceRegion;
100 return typeof replaceRegion === 'object'
101 && typeof replaceRegion.offset === 'number'
102 && typeof replaceRegion.length === 'number'
103 && typeof replaceRegion.text === 'string';
104}
105
106export interface ITextRegion {
107 offset: number;
108
109 length: number;
110}
111
112export function isTextRegion(value: unknown): value is ITextRegion {
113 const textRegion = value as ITextRegion;
114 return typeof textRegion === 'object'
115 && typeof textRegion.offset === 'number'
116 && typeof textRegion.length === 'number';
117}
118 24
119export const VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS = [ 25export function isConflictResult(result: unknown, conflictType: Conflict): boolean {
120 'TEXT', 26 const parsedConflictResult = serviceConflictResult.safeParse(result);
121 'METHOD', 27 return parsedConflictResult.success && parsedConflictResult.data.conflict === conflictType;
122 'FUNCTION',
123 'CONSTRUCTOR',
124 'FIELD',
125 'VARIABLE',
126 'CLASS',
127 'INTERFACE',
128 'MODULE',
129 'PROPERTY',
130 'UNIT',
131 'VALUE',
132 'ENUM',
133 'KEYWORD',
134 'SNIPPET',
135 'COLOR',
136 'FILE',
137 'REFERENCE',
138 'UNKNOWN',
139] as const;
140
141export type XtextContentAssistEntryKind = typeof VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS[number];
142
143export function isXtextContentAssistEntryKind(
144 value: unknown,
145): value is XtextContentAssistEntryKind {
146 return typeof value === 'string'
147 && VALID_XTEXT_CONTENT_ASSIST_ENTRY_KINDS.includes(value as XtextContentAssistEntryKind);
148} 28}
149 29
150export interface IContentAssistEntry { 30export const severity = z.enum(['error', 'warning', 'info', 'ignore']);
151 prefix: string;
152 31
153 proposal: string; 32export type Severity = z.infer<typeof severity>;
154 33
155 label?: string; 34export const issue = z.object({
35 description: z.string().nonempty(),
36 severity,
37 line: z.number().int(),
38 column: z.number().int().nonnegative(),
39 offset: z.number().int().nonnegative(),
40 length: z.number().int().nonnegative(),
41});
156 42
157 description?: string; 43export type Issue = z.infer<typeof issue>;
158 44
159 documentation?: string; 45export const validationResult = z.object({
46 issues: issue.array(),
47});
160 48
161 escapePosition?: number; 49export type ValidationResult = z.infer<typeof validationResult>;
162 50
163 textReplacements: IReplaceRegion[]; 51export const replaceRegion = z.object({
52 offset: z.number().int().nonnegative(),
53 length: z.number().int().nonnegative(),
54 text: z.string(),
55});
164 56
165 editPositions: ITextRegion[]; 57export type ReplaceRegion = z.infer<typeof replaceRegion>;
166 58
167 kind: XtextContentAssistEntryKind | string; 59export const textRegion = z.object({
168} 60 offset: z.number().int().nonnegative(),
61 length: z.number().int().nonnegative(),
62});
169 63
170function isStringOrUndefined(value: unknown): value is string | undefined { 64export type TextRegion = z.infer<typeof textRegion>;
171 return typeof value === 'string' || typeof value === 'undefined';
172}
173 65
174function isNumberOrUndefined(value: unknown): value is number | undefined { 66export const contentAssistEntry = z.object({
175 return typeof value === 'number' || typeof value === 'undefined'; 67 prefix: z.string(),
176} 68 proposal: z.string().nonempty(),
69 label: z.string().optional(),
70 description: z.string().nonempty().optional(),
71 documentation: z.string().nonempty().optional(),
72 escapePosition: z.number().int().nonnegative().optional(),
73 textReplacements: replaceRegion.array(),
74 editPositions: textRegion.array(),
75 kind: z.string().nonempty(),
76});
177 77
178export function isContentAssistEntry(value: unknown): value is IContentAssistEntry { 78export type ContentAssistEntry = z.infer<typeof contentAssistEntry>;
179 const entry = value as IContentAssistEntry;
180 return typeof entry === 'object'
181 && typeof entry.prefix === 'string'
182 && typeof entry.proposal === 'string'
183 && isStringOrUndefined(entry.label)
184 && isStringOrUndefined(entry.description)
185 && isStringOrUndefined(entry.documentation)
186 && isNumberOrUndefined(entry.escapePosition)
187 && isArrayOfType(entry.textReplacements, isReplaceRegion)
188 && isArrayOfType(entry.editPositions, isTextRegion)
189 && typeof entry.kind === 'string';
190}
191 79
192export interface IContentAssistResult extends IDocumentStateResult { 80export const contentAssistResult = documentStateResult.extend({
193 entries: IContentAssistEntry[]; 81 entries: contentAssistEntry.array(),
194} 82});
195 83
196export function isContentAssistResult(result: unknown): result is IContentAssistResult { 84export type ContentAssistResult = z.infer<typeof contentAssistResult>;
197 const contentAssistResult = result as IContentAssistResult;
198 return isDocumentStateResult(result)
199 && isArrayOfType(contentAssistResult.entries, isContentAssistEntry);
200}
201 85
202export interface IHighlightingRegion { 86export const highlightingRegion = z.object({
203 offset: number; 87 offset: z.number().int().nonnegative(),
88 length: z.number().int().nonnegative(),
89 styleClasses: z.string().nonempty().array(),
90});
204 91
205 length: number; 92export type HighlightingRegion = z.infer<typeof highlightingRegion>;
206 93
207 styleClasses: string[]; 94export const highlightingResult = z.object({
208} 95 regions: highlightingRegion.array(),
96});
209 97
210export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { 98export type HighlightingResult = z.infer<typeof highlightingResult>;
211 const region = value as IHighlightingRegion;
212 return typeof region === 'object'
213 && typeof region.offset === 'number'
214 && typeof region.length === 'number'
215 && isArrayOfType(region.styleClasses, (s): s is string => typeof s === 'string');
216}
217
218export interface IHighlightingResult {
219 regions: IHighlightingRegion[];
220}
221 99
222export function isHighlightingResult(result: unknown): result is IHighlightingResult { 100export const occurrencesResult = documentStateResult.extend({
223 const highlightingResult = result as IHighlightingResult; 101 writeRegions: textRegion.array(),
224 return typeof highlightingResult === 'object' 102 readRegions: textRegion.array(),
225 && isArrayOfType(highlightingResult.regions, isHighlightingRegion); 103});
226}
227 104
228export interface IOccurrencesResult extends IDocumentStateResult { 105export type OccurrencesResult = z.infer<typeof occurrencesResult>;
229 writeRegions: ITextRegion[];
230 106
231 readRegions: ITextRegion[]; 107export const formattingResult = documentStateResult.extend({
232} 108 formattedText: z.string(),
109 replaceRegion: textRegion,
110});
233 111
234export function isOccurrencesResult(result: unknown): result is IOccurrencesResult { 112export type FormattingResult = z.infer<typeof formattingResult>;
235 const occurrencesResult = result as IOccurrencesResult;
236 return isDocumentStateResult(occurrencesResult)
237 && isArrayOfType(occurrencesResult.writeRegions, isTextRegion)
238 && isArrayOfType(occurrencesResult.readRegions, isTextRegion);
239}
diff --git a/language-web/yarn.lock b/language-web/yarn.lock
index 06b30508..ebebe1ff 100644
--- a/language-web/yarn.lock
+++ b/language-web/yarn.lock
@@ -7784,3 +7784,8 @@ yocto-queue@^0.1.0:
7784 version "0.1.0" 7784 version "0.1.0"
7785 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 7785 resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
7786 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 7786 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
7787
7788zod@^3.11.6:
7789 version "3.11.6"
7790 resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"
7791 integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==
diff --git a/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2 b/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2
index 58620d6a..0d934b68 100644
--- a/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2
+++ b/language/src/main/java/tools/refinery/language/GenerateProblem.mwe2
@@ -39,6 +39,9 @@ Workflow {
39 serializer = { 39 serializer = {
40 generateStub = false 40 generateStub = false
41 } 41 }
42 formatter = {
43 generateStub = true
44 }
42 validator = { 45 validator = {
43 generateDeprecationValidation = true 46 generateDeprecationValidation = true
44 } 47 }
diff --git a/language/src/main/java/tools/refinery/language/Problem.xtext b/language/src/main/java/tools/refinery/language/Problem.xtext
index 26773047..c94d40ab 100644
--- a/language/src/main/java/tools/refinery/language/Problem.xtext
+++ b/language/src/main/java/tools/refinery/language/Problem.xtext
@@ -8,7 +8,8 @@ Problem:
8 statements+=Statement*; 8 statements+=Statement*;
9 9
10Statement: 10Statement:
11 ClassDeclaration | EnumDeclaration | PredicateDefinition | RuleDefinition | Assertion | NodeValueAssertion | ScopeDeclaration | 11 ClassDeclaration | EnumDeclaration | PredicateDefinition | RuleDefinition | Assertion | NodeValueAssertion |
12 ScopeDeclaration |
12 IndividualDeclaration; 13 IndividualDeclaration;
13 14
14ClassDeclaration: 15ClassDeclaration:
@@ -67,7 +68,7 @@ Literal:
67 68
68ValueLiteral: 69ValueLiteral:
69 atom=Atom 70 atom=Atom
70 (refinement?=":"|"=") 71 (refinement?=":" | "=")
71 values+=LogicConstant ("|" values+=LogicConstant)*; 72 values+=LogicConstant ("|" values+=LogicConstant)*;
72 73
73NegativeLiteral: 74NegativeLiteral:
@@ -78,7 +79,7 @@ ActionLiteral:
78 79
79ValueActionLiteral: 80ValueActionLiteral:
80 atom=Atom 81 atom=Atom
81 (refinement?=":"|"=") 82 (refinement?=":" | "=")
82 value=LogicValue; 83 value=LogicValue;
83 84
84DeleteActionLiteral: 85DeleteActionLiteral:
@@ -86,7 +87,7 @@ DeleteActionLiteral:
86 87
87NewActionLiteral: 88NewActionLiteral:
88 "new" variable=NewVariable; 89 "new" variable=NewVariable;
89 90
90NewVariable: 91NewVariable:
91 name=Identifier; 92 name=Identifier;
92 93
diff --git a/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java b/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
new file mode 100644
index 00000000..903347f7
--- /dev/null
+++ b/language/src/main/java/tools/refinery/language/formatting2/ProblemFormatter.java
@@ -0,0 +1,183 @@
1/*
2 * generated by Xtext 2.26.0.M2
3 */
4package tools.refinery.language.formatting2;
5
6import org.eclipse.emf.ecore.EObject;
7import org.eclipse.xtext.formatting2.AbstractJavaFormatter;
8import org.eclipse.xtext.formatting2.IFormattableDocument;
9import org.eclipse.xtext.formatting2.IHiddenRegionFormatter;
10import org.eclipse.xtext.formatting2.regionaccess.ISemanticRegionsFinder;
11import org.eclipse.xtext.formatting2.regionaccess.ISequentialRegion;
12import org.eclipse.xtext.xbase.lib.Procedures.Procedure1;
13
14import tools.refinery.language.model.problem.Assertion;
15import tools.refinery.language.model.problem.Atom;
16import tools.refinery.language.model.problem.ClassDeclaration;
17import tools.refinery.language.model.problem.Conjunction;
18import tools.refinery.language.model.problem.IndividualDeclaration;
19import tools.refinery.language.model.problem.NegativeLiteral;
20import tools.refinery.language.model.problem.Parameter;
21import tools.refinery.language.model.problem.PredicateDefinition;
22import tools.refinery.language.model.problem.Problem;
23import tools.refinery.language.model.problem.ProblemPackage;
24
25public class ProblemFormatter extends AbstractJavaFormatter {
26
27 protected void format(Problem problem, IFormattableDocument doc) {
28 doc.prepend(problem, this::noSpace);
29 var region = regionFor(problem);
30 doc.append(region.keyword("problem"), this::oneSpace);
31 doc.prepend(region.keyword("."), this::noSpace);
32 appendNewLines(doc, region.keyword("."), this::twoNewLines);
33 for (var statement : problem.getStatements()) {
34 doc.format(statement);
35 }
36 }
37
38 protected void format(Assertion assertion, IFormattableDocument doc) {
39 surroundNewLines(doc, assertion, this::singleNewLine);
40 var region = regionFor(assertion);
41 doc.append(region.feature(ProblemPackage.Literals.ASSERTION__DEFAULT), this::oneSpace);
42 doc.append(region.feature(ProblemPackage.Literals.ASSERTION__VALUE), this::noSpace);
43 doc.append(region.feature(ProblemPackage.Literals.ASSERTION__RELATION), this::noSpace);
44 formatParenthesizedList(region, doc);
45 doc.prepend(region.keyword(":"), this::noSpace);
46 doc.append(region.keyword(":"), this::oneSpace);
47 doc.prepend(region.keyword("."), this::noSpace);
48 for (var argument : assertion.getArguments()) {
49 doc.format(argument);
50 }
51 }
52
53 protected void format(ClassDeclaration classDeclaration, IFormattableDocument doc) {
54 surroundNewLines(doc, classDeclaration, this::twoNewLines);
55 var region = regionFor(classDeclaration);
56 doc.append(region.feature(ProblemPackage.Literals.CLASS_DECLARATION__ABSTRACT), this::oneSpace);
57 doc.append(region.keyword("class"), this::oneSpace);
58 doc.surround(region.keyword("extends"), this::oneSpace);
59 formatList(region, ",", doc);
60 doc.prepend(region.keyword("{"), this::oneSpace);
61 doc.append(region.keyword("{"), it -> it.setNewLines(1, 1, 2));
62 doc.prepend(region.keyword("}"), it -> it.setNewLines(1, 1, 2));
63 doc.prepend(region.keyword("."), this::noSpace);
64 for (var referenceDeclaration : classDeclaration.getReferenceDeclarations()) {
65 doc.format(referenceDeclaration);
66 }
67 }
68
69 protected void format(PredicateDefinition predicateDefinition, IFormattableDocument doc) {
70 surroundNewLines(doc, predicateDefinition, this::twoNewLines);
71 var region = regionFor(predicateDefinition);
72 doc.append(region.feature(ProblemPackage.Literals.PREDICATE_DEFINITION__KIND), this::oneSpace);
73 doc.append(region.keyword("pred"), this::oneSpace);
74 doc.append(region.feature(ProblemPackage.Literals.NAMED_ELEMENT__NAME), this::noSpace);
75 formatParenthesizedList(region, doc);
76 doc.surround(region.keyword("<->"), this::oneSpace);
77 formatList(region, ";", doc);
78 doc.prepend(region.keyword("."), this::noSpace);
79 for (var parameter : predicateDefinition.getParameters()) {
80 doc.format(parameter);
81 }
82 for (var body : predicateDefinition.getBodies()) {
83 doc.format(body);
84 }
85 }
86
87 protected void format(Parameter parameter, IFormattableDocument doc) {
88 doc.append(regionFor(parameter).feature(ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE), this::oneSpace);
89 }
90
91 protected void format(Conjunction conjunction, IFormattableDocument doc) {
92 var region = regionFor(conjunction);
93 formatList(region, ",", doc);
94 for (var literal : conjunction.getLiterals()) {
95 doc.format(literal);
96 }
97 }
98
99 protected void format(NegativeLiteral literal, IFormattableDocument doc) {
100 var region = regionFor(literal);
101 doc.append(region.keyword("!"), this::noSpace);
102 doc.format(literal.getAtom());
103 }
104
105 protected void format(Atom atom, IFormattableDocument doc) {
106 var region = regionFor(atom);
107 doc.append(region.feature(ProblemPackage.Literals.ATOM__RELATION), this::noSpace);
108 doc.append(region.feature(ProblemPackage.Literals.ATOM__TRANSITIVE_CLOSURE), this::noSpace);
109 formatParenthesizedList(region, doc);
110 for (var argument : atom.getArguments()) {
111 doc.format(argument);
112 }
113 }
114
115 protected void format(IndividualDeclaration individualDeclaration, IFormattableDocument doc) {
116 surroundNewLines(doc, individualDeclaration, this::singleNewLine);
117 var region = regionFor(individualDeclaration);
118 doc.append(region.keyword("indiv"), this::oneSpace);
119 formatList(region, ",", doc);
120 doc.prepend(region.keyword("."), this::noSpace);
121 }
122
123 protected void formatParenthesizedList(ISemanticRegionsFinder region, IFormattableDocument doc) {
124 doc.append(region.keyword("("), this::noSpace);
125 doc.prepend(region.keyword(")"), this::noSpace);
126 formatList(region, ",", doc);
127 }
128
129 protected void formatList(ISemanticRegionsFinder region, String separator, IFormattableDocument doc) {
130 for (var comma : region.keywords(separator)) {
131 doc.prepend(comma, this::noSpace);
132 doc.append(comma, this::oneSpace);
133 }
134 }
135
136 protected void singleNewLine(IHiddenRegionFormatter it) {
137 it.setNewLines(1, 1, 2);
138 }
139
140 protected void twoNewLines(IHiddenRegionFormatter it) {
141 it.highPriority();
142 it.setNewLines(2);
143 }
144
145 protected void surroundNewLines(IFormattableDocument doc, EObject eObject,
146 Procedure1<? super IHiddenRegionFormatter> init) {
147 var region = doc.getRequest().getTextRegionAccess().regionForEObject(eObject);
148 preprendNewLines(doc, region, init);
149 appendNewLines(doc, region, init);
150 }
151
152 protected void preprendNewLines(IFormattableDocument doc, ISequentialRegion region,
153 Procedure1<? super IHiddenRegionFormatter> init) {
154 if (region == null) {
155 return;
156 }
157 var previousHiddenRegion = region.getPreviousHiddenRegion();
158 if (previousHiddenRegion == null) {
159 return;
160 }
161 if (previousHiddenRegion.getPreviousSequentialRegion() == null) {
162 doc.set(previousHiddenRegion, it -> it.setNewLines(0));
163 } else {
164 doc.set(previousHiddenRegion, init);
165 }
166 }
167
168 protected void appendNewLines(IFormattableDocument doc, ISequentialRegion region,
169 Procedure1<? super IHiddenRegionFormatter> init) {
170 if (region == null) {
171 return;
172 }
173 var nextHiddenRegion = region.getNextHiddenRegion();
174 if (nextHiddenRegion == null) {
175 return;
176 }
177 if (nextHiddenRegion.getNextSequentialRegion() == null) {
178 doc.set(nextHiddenRegion, it -> it.setNewLines(1));
179 } else {
180 doc.set(nextHiddenRegion, init);
181 }
182 }
183}
diff --git a/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java b/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java
new file mode 100644
index 00000000..41ad2d31
--- /dev/null
+++ b/language/src/test/java/tools/refinery/language/tests/formatting2/ProblemFormatterTest.java
@@ -0,0 +1,235 @@
1package tools.refinery.language.tests.formatting2;
2
3import static org.hamcrest.MatcherAssert.assertThat;
4import static org.hamcrest.Matchers.equalTo;
5
6import java.util.List;
7
8import org.eclipse.xtext.formatting2.FormatterRequest;
9import org.eclipse.xtext.formatting2.IFormatter2;
10import org.eclipse.xtext.formatting2.regionaccess.ITextRegionAccess;
11import org.eclipse.xtext.formatting2.regionaccess.ITextReplacement;
12import org.eclipse.xtext.formatting2.regionaccess.TextRegionAccessBuilder;
13import org.eclipse.xtext.resource.XtextResource;
14import org.eclipse.xtext.testing.InjectWith;
15import org.eclipse.xtext.testing.extensions.InjectionExtension;
16import org.eclipse.xtext.testing.util.ParseHelper;
17import org.junit.jupiter.api.Test;
18import org.junit.jupiter.api.extension.ExtendWith;
19
20import com.google.inject.Inject;
21import com.google.inject.Provider;
22
23import tools.refinery.language.model.problem.Problem;
24import tools.refinery.language.tests.ProblemInjectorProvider;
25
26@ExtendWith(InjectionExtension.class)
27@InjectWith(ProblemInjectorProvider.class)
28class ProblemFormatterTest {
29 @Inject
30 private ParseHelper<Problem> parseHelper;
31
32 @Inject
33 private Provider<FormatterRequest> formatterRequestProvider;
34
35 @Inject
36 private TextRegionAccessBuilder regionBuilder;
37
38 @Inject
39 private IFormatter2 formatter2;
40
41 @Test
42 void problemNameTest() {
43 testFormatter(" problem problem . ", "problem problem.\n");
44 }
45
46 @Test
47 void assertionTest() {
48 testFormatter(" equals ( a , b , * ) : true . ", "equals(a, b, *): true.\n");
49 }
50
51 @Test
52 void defaultAssertionTest() {
53 testFormatter(" default equals ( a , b , * ) : true . ", "default equals(a, b, *): true.\n");
54 }
55
56 @Test
57 void assertionShortTrueTest() {
58 testFormatter(" equals ( a , b , * ) . ", "equals(a, b, *).\n");
59 }
60
61 @Test
62 void defaultAssertionShortTrueTest() {
63 testFormatter(" default equals ( a , b , * ) . ", "default equals(a, b, *).\n");
64 }
65
66 @Test
67 void assertionShortFalseTest() {
68 testFormatter(" ! equals ( a , b , * ) . ", "!equals(a, b, *).\n");
69 }
70
71 @Test
72 void defaultAssertionShortFalseTest() {
73 testFormatter(" default ! equals ( a , b , * ) . ", "default !equals(a, b, *).\n");
74 }
75
76 @Test
77 void assertionShortUnknownTest() {
78 testFormatter(" ? equals ( a , b , * ) . ", "?equals(a, b, *).\n");
79 }
80
81 @Test
82 void defaultAssertionShortUnknownTest() {
83 testFormatter(" default ? equals ( a , b , * ) . ", "default ?equals(a, b, *).\n");
84 }
85
86 @Test
87 void multipleAssertionsTest() {
88 testFormatter(" exists ( a ) . ? equals ( a , a ).", """
89 exists(a).
90 ?equals(a, a).
91 """);
92 }
93
94 @Test
95 void multipleAssertionsNamedProblemTest() {
96 testFormatter(" problem foo . exists ( a ) . ? equals ( a , a ).", """
97 problem foo.
98
99 exists(a).
100 ?equals(a, a).
101 """);
102 }
103
104 @Test
105 void classWithoutBodyTest() {
106 testFormatter(" class Foo . ", "class Foo.\n");
107 }
108
109 @Test
110 void abstractClassWithoutBodyTest() {
111 testFormatter(" abstract class Foo . ", "abstract class Foo.\n");
112 }
113
114 @Test
115 void classExtendsWithoutBodyTest() {
116 testFormatter(" class Foo. class Bar . class Quux extends Foo , Bar . ", """
117 class Foo.
118
119 class Bar.
120
121 class Quux extends Foo, Bar.
122 """);
123 }
124
125 @Test
126 void classWithEmptyBodyTest() {
127 testFormatter(" class Foo { } ", """
128 class Foo {
129 }
130 """);
131 }
132
133 @Test
134 void classExtendsWithBodyTest() {
135 testFormatter(" class Foo. class Bar . class Quux extends Foo , Bar { } ", """
136 class Foo.
137
138 class Bar.
139
140 class Quux extends Foo, Bar {
141 }
142 """);
143 }
144
145 @Test
146 void predicateWithoutBodyTest() {
147 testFormatter(" pred foo ( node a , b ) . ", "pred foo(node a, b).\n");
148 }
149
150 @Test
151 void predicateWithBodyTest() {
152 testFormatter(
153 " pred foo ( node a , b ) <-> equal (a , _c ) , ! equal ( a , b ) ; equal+( a , b ) . ",
154 "pred foo(node a, b) <-> equal(a, _c), !equal(a, b); equal+(a, b).\n");
155 }
156
157 @Test
158 void predicatesWithoutBodyTest() {
159 testFormatter(" pred foo ( node a , b ) . pred bar ( node c ) . ", """
160 pred foo(node a, b).
161
162 pred bar(node c).
163 """);
164 }
165
166 @Test
167 void predicateCommentsTest() {
168 testFormatter("""
169 % Some foo
170 pred foo ( node a , b ) .
171 % Some bar
172 pred bar ( node c ) .
173 """, """
174 % Some foo
175 pred foo(node a, b).
176
177 % Some bar
178 pred bar(node c).
179 """);
180 }
181
182 @Test
183 void individualDeclarationTest() {
184 testFormatter(" indiv a , b . ", "indiv a, b.\n");
185 }
186
187 @Test
188 void mixedDeclarationsTest() {
189 testFormatter("""
190 problem test.
191 pred foo(node a).
192 class Foo.
193 foo(n1, n2).
194 indiv i1.
195 !foo(i1, n1).
196 pred bar(node a, node b).
197 pred quux().
198 default !bar(*, *).
199 """, """
200 problem test.
201
202 pred foo(node a).
203
204 class Foo.
205
206 foo(n1, n2).
207 indiv i1.
208 !foo(i1, n1).
209
210 pred bar(node a, node b).
211
212 pred quux().
213
214 default !bar(*, *).
215 """);
216 }
217
218 private void testFormatter(String toFormat, String expected) {
219 Problem problem;
220 try {
221 problem = parseHelper.parse(toFormat);
222 } catch (Exception e) {
223 throw new RuntimeException("Failed to parse document", e);
224 }
225 var resource = (XtextResource) problem.eResource();
226 FormatterRequest request = formatterRequestProvider.get();
227 request.setAllowIdentityEdits(false);
228 request.setFormatUndefinedHiddenRegionsOnly(false);
229 ITextRegionAccess regionAccess = regionBuilder.forNodeModel(resource).create();
230 request.setTextRegionAccess(regionAccess);
231 List<ITextReplacement> replacements = formatter2.format(request);
232 var formattedString = regionAccess.getRewriter().renderToString(replacements);
233 assertThat(formattedString, equalTo(expected));
234 }
235}
diff --git a/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java b/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
index 22c79a09..ba3aaeb7 100644
--- a/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
+++ b/language/src/test/java/tools/refinery/language/tests/serializer/ProblemSerializerTest.java
@@ -38,16 +38,16 @@ import tools.refinery.language.tests.ProblemInjectorProvider;
38@InjectWith(ProblemInjectorProvider.class) 38@InjectWith(ProblemInjectorProvider.class)
39class ProblemSerializerTest { 39class ProblemSerializerTest {
40 @Inject 40 @Inject
41 ResourceSet resourceSet; 41 private ResourceSet resourceSet;
42 42
43 @Inject 43 @Inject
44 ProblemTestUtil testUtil; 44 private ProblemTestUtil testUtil;
45 45
46 Resource resource; 46 private Resource resource;
47 47
48 Problem problem; 48 private Problem problem;
49 49
50 Problem builtin; 50 private Problem builtin;
51 51
52 @BeforeEach 52 @BeforeEach
53 void beforeEach() { 53 void beforeEach() {
@@ -68,14 +68,16 @@ class ProblemSerializerTest {
68 problem.getStatements().add(individualDeclaration); 68 problem.getStatements().add(individualDeclaration);
69 createAssertion(pred, node, value); 69 createAssertion(pred, node, value);
70 70
71 assertSerializedResult("pred foo ( node p ) . indiv a . " + serializedAssertion); 71 assertSerializedResult("""
72 pred foo(node p).
73
74 indiv a.
75 """ + serializedAssertion + "\n");
72 } 76 }
73 77
74 static Stream<Arguments> assertionTest() { 78 static Stream<Arguments> assertionTest() {
75 return Stream.of(Arguments.of(LogicValue.TRUE, "foo ( a ) ."), 79 return Stream.of(Arguments.of(LogicValue.TRUE, "foo(a)."), Arguments.of(LogicValue.FALSE, "!foo(a)."),
76 Arguments.of(LogicValue.FALSE, "! foo ( a ) ."), 80 Arguments.of(LogicValue.UNKNOWN, "?foo(a)."), Arguments.of(LogicValue.ERROR, "foo(a): error."));
77 Arguments.of(LogicValue.UNKNOWN, "? foo ( a ) ."),
78 Arguments.of(LogicValue.ERROR, "foo ( a ) : error ."));
79 } 81 }
80 82
81 @Test 83 @Test
@@ -86,7 +88,11 @@ class ProblemSerializerTest {
86 problem.getNodes().add(node); 88 problem.getNodes().add(node);
87 createAssertion(pred, node); 89 createAssertion(pred, node);
88 90
89 assertSerializedResult("pred foo ( node p ) . foo ( a ) ."); 91 assertSerializedResult("""
92 pred foo(node p).
93
94 foo(a).
95 """);
90 } 96 }
91 97
92 private PredicateDefinition createPred() { 98 private PredicateDefinition createPred() {
@@ -111,7 +117,11 @@ class ProblemSerializerTest {
111 problem.getStatements().add(classDeclaration); 117 problem.getStatements().add(classDeclaration);
112 createAssertion(classDeclaration, newNode); 118 createAssertion(classDeclaration, newNode);
113 119
114 assertSerializedResult("class Foo . Foo ( Foo::new ) ."); 120 assertSerializedResult("""
121 class Foo.
122
123 Foo(Foo::new).
124 """);
115 } 125 }
116 126
117 private void createAssertion(Relation relation, Node node) { 127 private void createAssertion(Relation relation, Node node) {
@@ -151,7 +161,9 @@ class ProblemSerializerTest {
151 pred.getBodies().add(conjunction); 161 pred.getBodies().add(conjunction);
152 problem.getStatements().add(pred); 162 problem.getStatements().add(pred);
153 163
154 assertSerializedResult("pred foo ( node p1 , node p2 ) <-> equals ( p1 , q ) , equals ( q , p2 ) ."); 164 assertSerializedResult("""
165 pred foo(node p1, node p2) <-> equals(p1, q), equals(q, p2).
166 """);
155 } 167 }
156 168
157 private Atom createAtom(Relation relation, VariableOrNode variable1, VariableOrNode variable2) { 169 private Atom createAtom(Relation relation, VariableOrNode variable1, VariableOrNode variable2) {
@@ -192,7 +204,9 @@ class ProblemSerializerTest {
192 pred.getBodies().add(conjunction); 204 pred.getBodies().add(conjunction);
193 problem.getStatements().add(pred); 205 problem.getStatements().add(pred);
194 206
195 assertSerializedResult("pred foo ( node p ) <-> equals ( p , _q ) ."); 207 assertSerializedResult("""
208 pred foo(node p) <-> equals(p, _q).
209 """);
196 } 210 }
197 211
198 private void assertSerializedResult(String expected) { 212 private void assertSerializedResult(String expected) {