diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-10-30 20:14:50 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 19:26:14 +0100 |
commit | cbf442d8fd9f72c567ebf9f036a219a9ff100487 (patch) | |
tree | a128345d9a9863bef7f3670da585bcb62d13cb34 /language-web | |
parent | feat(web): show error count on generate button (diff) | |
download | refinery-cbf442d8fd9f72c567ebf9f036a219a9ff100487.tar.gz refinery-cbf442d8fd9f72c567ebf9f036a219a9ff100487.tar.zst refinery-cbf442d8fd9f72c567ebf9f036a219a9ff100487.zip |
feat(web): semantic highlighting
Diffstat (limited to 'language-web')
12 files changed, 163 insertions, 15 deletions
diff --git a/language-web/package.json b/language-web/package.json index 9185f8fe..76d27865 100644 --- a/language-web/package.json +++ b/language-web/package.json | |||
@@ -75,6 +75,7 @@ | |||
75 | "@codemirror/language": "^0.19.3", | 75 | "@codemirror/language": "^0.19.3", |
76 | "@codemirror/lint": "^0.19.2", | 76 | "@codemirror/lint": "^0.19.2", |
77 | "@codemirror/matchbrackets": "^0.19.3", | 77 | "@codemirror/matchbrackets": "^0.19.3", |
78 | "@codemirror/rangeset": "^0.19.1", | ||
78 | "@codemirror/rectangular-selection": "^0.19.1", | 79 | "@codemirror/rectangular-selection": "^0.19.1", |
79 | "@codemirror/search": "^0.19.2", | 80 | "@codemirror/search": "^0.19.2", |
80 | "@codemirror/state": "^0.19.0", | 81 | "@codemirror/state": "^0.19.0", |
diff --git a/language-web/src/main/js/editor/EditorParent.ts b/language-web/src/main/js/editor/EditorParent.ts index a19759a4..ea8c13b6 100644 --- a/language-web/src/main/js/editor/EditorParent.ts +++ b/language-web/src/main/js/editor/EditorParent.ts | |||
@@ -133,6 +133,34 @@ export const EditorParent = styled('div')(({ theme }) => { | |||
133 | '.cmt-variableName': { | 133 | '.cmt-variableName': { |
134 | color: '#c8ae9d', | 134 | color: '#c8ae9d', |
135 | }, | 135 | }, |
136 | '.cmt-problem-node': { | ||
137 | '&, & .cmt-variableName': { | ||
138 | color: theme.palette.text.secondary, | ||
139 | }, | ||
140 | }, | ||
141 | '.cmt-problem-unique': { | ||
142 | '&, & .cmt-variableName': { | ||
143 | color: theme.palette.text.primary, | ||
144 | }, | ||
145 | }, | ||
146 | '.cmt-problem-abstract, .cmt-problem-new': { | ||
147 | fontStyle: 'italic', | ||
148 | }, | ||
149 | '.cmt-problem-containment': { | ||
150 | fontWeight: 700, | ||
151 | }, | ||
152 | '.cmt-problem-error': { | ||
153 | '&, & .cmt-typeName': { | ||
154 | color: theme.palette.error.main, | ||
155 | }, | ||
156 | }, | ||
157 | '.cmt-problem-builtin': { | ||
158 | '&, & .cmt-typeName, & .cmt-atom, & .cmt-variableName': { | ||
159 | color: theme.palette.primary.main, | ||
160 | fontWeight: 400, | ||
161 | fontStyle: 'normal', | ||
162 | }, | ||
163 | }, | ||
136 | '.cm-tooltip-autocomplete': { | 164 | '.cm-tooltip-autocomplete': { |
137 | background: theme.palette.background.paper, | 165 | background: theme.palette.background.paper, |
138 | boxShadow: `0px 2px 4px -1px rgb(0 0 0 / 20%), | 166 | boxShadow: `0px 2px 4px -1px rgb(0 0 0 / 20%), |
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts index 78cf763c..f47f47a0 100644 --- a/language-web/src/main/js/editor/EditorStore.ts +++ b/language-web/src/main/js/editor/EditorStore.ts | |||
@@ -30,6 +30,7 @@ import { | |||
30 | TransactionSpec, | 30 | TransactionSpec, |
31 | } from '@codemirror/state'; | 31 | } from '@codemirror/state'; |
32 | import { | 32 | import { |
33 | DecorationSet, | ||
33 | drawSelection, | 34 | drawSelection, |
34 | EditorView, | 35 | EditorView, |
35 | highlightActiveLine, | 36 | highlightActiveLine, |
@@ -43,8 +44,9 @@ import { | |||
43 | } from 'mobx'; | 44 | } from 'mobx'; |
44 | 45 | ||
45 | import { problemLanguageSupport } from '../language/problemLanguageSupport'; | 46 | import { problemLanguageSupport } from '../language/problemLanguageSupport'; |
46 | import { getLogger } from '../utils/logger'; | 47 | import { semanticHighlighting, setSemanticHighlighting } from './semanticHighlighting'; |
47 | import type { ThemeStore } from '../theme/ThemeStore'; | 48 | import type { ThemeStore } from '../theme/ThemeStore'; |
49 | import { getLogger } from '../utils/logger'; | ||
48 | import { XtextClient } from '../xtext/XtextClient'; | 50 | import { XtextClient } from '../xtext/XtextClient'; |
49 | 51 | ||
50 | const log = getLogger('editor.EditorStore'); | 52 | const log = getLogger('editor.EditorStore'); |
@@ -103,6 +105,7 @@ export class EditorStore { | |||
103 | top: true, | 105 | top: true, |
104 | matchCase: true, | 106 | matchCase: true, |
105 | }), | 107 | }), |
108 | semanticHighlighting, | ||
106 | // We add the gutters to `extensions` in the order we want them to appear. | 109 | // We add the gutters to `extensions` in the order we want them to appear. |
107 | foldGutter(), | 110 | foldGutter(), |
108 | lineNumbers(), | 111 | lineNumbers(), |
@@ -201,6 +204,10 @@ export class EditorStore { | |||
201 | return null; | 204 | return null; |
202 | } | 205 | } |
203 | 206 | ||
207 | updateSemanticHighlighting(decorations: DecorationSet): void { | ||
208 | this.dispatch(setSemanticHighlighting(decorations)); | ||
209 | } | ||
210 | |||
204 | /** | 211 | /** |
205 | * @returns `true` if there is history to undo | 212 | * @returns `true` if there is history to undo |
206 | */ | 213 | */ |
diff --git a/language-web/src/main/js/editor/semanticHighlighting.ts b/language-web/src/main/js/editor/semanticHighlighting.ts new file mode 100644 index 00000000..2d6804f8 --- /dev/null +++ b/language-web/src/main/js/editor/semanticHighlighting.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; | ||
2 | import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; | ||
3 | |||
4 | const setSemanticHighlightingEffect = StateEffect.define<DecorationSet>(); | ||
5 | |||
6 | export function setSemanticHighlighting(decorations: DecorationSet): TransactionSpec { | ||
7 | return { | ||
8 | effects: [ | ||
9 | setSemanticHighlightingEffect.of(decorations), | ||
10 | ], | ||
11 | }; | ||
12 | } | ||
13 | |||
14 | export const semanticHighlighting = StateField.define<DecorationSet>({ | ||
15 | create() { | ||
16 | return Decoration.none; | ||
17 | }, | ||
18 | update(currentDecorations, transaction) { | ||
19 | let newDecorations: DecorationSet | null = null; | ||
20 | transaction.effects.forEach((effect) => { | ||
21 | if (effect.is(setSemanticHighlightingEffect)) { | ||
22 | newDecorations = effect.value; | ||
23 | } | ||
24 | }); | ||
25 | if (newDecorations === null) { | ||
26 | if (transaction.docChanged) { | ||
27 | return currentDecorations.map(transaction.changes); | ||
28 | } | ||
29 | return currentDecorations; | ||
30 | } | ||
31 | return newDecorations; | ||
32 | }, | ||
33 | provide: (f) => EditorView.decorations.from(f), | ||
34 | }); | ||
diff --git a/language-web/src/main/js/index.tsx b/language-web/src/main/js/index.tsx index 1b24eadb..13a62af0 100644 --- a/language-web/src/main/js/index.tsx +++ b/language-web/src/main/js/index.tsx | |||
@@ -24,7 +24,7 @@ enum TaxStatus { | |||
24 | } | 24 | } |
25 | 25 | ||
26 | % A child cannot have any dependents. | 26 | % A child cannot have any dependents. |
27 | error invalidTaxStatus(Person p) <-> | 27 | pred invalidTaxStatus(Person p) <-> |
28 | taxStatus(p, child), | 28 | taxStatus(p, child), |
29 | children(p, _q) | 29 | children(p, _q) |
30 | ; taxStatus(p, retired), | 30 | ; taxStatus(p, retired), |
diff --git a/language-web/src/main/js/xtext/HighlightingService.ts b/language-web/src/main/js/xtext/HighlightingService.ts new file mode 100644 index 00000000..b8ceed20 --- /dev/null +++ b/language-web/src/main/js/xtext/HighlightingService.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { Decoration } from '@codemirror/view'; | ||
2 | import { Range, RangeSet } from '@codemirror/rangeset'; | ||
3 | |||
4 | import type { EditorStore } from '../editor/EditorStore'; | ||
5 | import type { UpdateService } from './UpdateService'; | ||
6 | import { getLogger } from '../utils/logger'; | ||
7 | import { isHighlightingResult } from './xtextServiceResults'; | ||
8 | |||
9 | const log = getLogger('xtext.ValidationService'); | ||
10 | |||
11 | export class HighlightingService { | ||
12 | private store: EditorStore; | ||
13 | |||
14 | private updateService: UpdateService; | ||
15 | |||
16 | constructor(store: EditorStore, updateService: UpdateService) { | ||
17 | this.store = store; | ||
18 | this.updateService = updateService; | ||
19 | } | ||
20 | |||
21 | onPush(push: unknown): void { | ||
22 | if (!isHighlightingResult(push)) { | ||
23 | log.error('Invalid highlighting result', push); | ||
24 | return; | ||
25 | } | ||
26 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | ||
27 | const decorations: Range<Decoration>[] = []; | ||
28 | push.regions.forEach(({ offset, length, styleClasses }) => { | ||
29 | if (styleClasses.length === 0) { | ||
30 | return; | ||
31 | } | ||
32 | const from = allChanges.mapPos(offset); | ||
33 | const to = allChanges.mapPos(offset + length); | ||
34 | if (to <= from) { | ||
35 | return; | ||
36 | } | ||
37 | decorations.push(Decoration.mark({ | ||
38 | class: styleClasses.map((c) => `cmt-problem-${c}`).join(' '), | ||
39 | }).range(from, to)); | ||
40 | }); | ||
41 | this.store.updateSemanticHighlighting(RangeSet.of(decorations, true)); | ||
42 | } | ||
43 | } | ||
diff --git a/language-web/src/main/js/xtext/ValidationService.ts b/language-web/src/main/js/xtext/ValidationService.ts index 163b5535..31c8f716 100644 --- a/language-web/src/main/js/xtext/ValidationService.ts +++ b/language-web/src/main/js/xtext/ValidationService.ts | |||
@@ -24,15 +24,20 @@ export class ValidationService { | |||
24 | } | 24 | } |
25 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | 25 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); |
26 | const diagnostics: Diagnostic[] = []; | 26 | const diagnostics: Diagnostic[] = []; |
27 | push.issues.forEach((issue) => { | 27 | push.issues.forEach(({ |
28 | if (issue.severity === 'ignore') { | 28 | offset, |
29 | length, | ||
30 | severity, | ||
31 | description, | ||
32 | }) => { | ||
33 | if (severity === 'ignore') { | ||
29 | return; | 34 | return; |
30 | } | 35 | } |
31 | diagnostics.push({ | 36 | diagnostics.push({ |
32 | from: allChanges.mapPos(issue.offset), | 37 | from: allChanges.mapPos(offset), |
33 | to: allChanges.mapPos(issue.offset + issue.length), | 38 | to: allChanges.mapPos(offset + length), |
34 | severity: issue.severity, | 39 | severity, |
35 | message: issue.description, | 40 | message: description, |
36 | }); | 41 | }); |
37 | }); | 42 | }); |
38 | this.store.updateDiagnostics(diagnostics); | 43 | this.store.updateDiagnostics(diagnostics); |
diff --git a/language-web/src/main/js/xtext/XtextClient.ts b/language-web/src/main/js/xtext/XtextClient.ts index 7683a8ef..ccb58ab4 100644 --- a/language-web/src/main/js/xtext/XtextClient.ts +++ b/language-web/src/main/js/xtext/XtextClient.ts | |||
@@ -6,6 +6,7 @@ import type { Transaction } from '@codemirror/state'; | |||
6 | 6 | ||
7 | import type { EditorStore } from '../editor/EditorStore'; | 7 | import type { EditorStore } from '../editor/EditorStore'; |
8 | import { ContentAssistService } from './ContentAssistService'; | 8 | import { ContentAssistService } from './ContentAssistService'; |
9 | import { HighlightingService } from './HighlightingService'; | ||
9 | import { UpdateService } from './UpdateService'; | 10 | import { UpdateService } from './UpdateService'; |
10 | import { getLogger } from '../utils/logger'; | 11 | import { getLogger } from '../utils/logger'; |
11 | import { ValidationService } from './ValidationService'; | 12 | import { ValidationService } from './ValidationService'; |
@@ -20,6 +21,8 @@ export class XtextClient { | |||
20 | 21 | ||
21 | private contentAssistService: ContentAssistService; | 22 | private contentAssistService: ContentAssistService; |
22 | 23 | ||
24 | private highlightingService: HighlightingService; | ||
25 | |||
23 | private validationService: ValidationService; | 26 | private validationService: ValidationService; |
24 | 27 | ||
25 | constructor(store: EditorStore) { | 28 | constructor(store: EditorStore) { |
@@ -29,6 +32,7 @@ export class XtextClient { | |||
29 | ); | 32 | ); |
30 | this.updateService = new UpdateService(store, this.webSocketClient); | 33 | this.updateService = new UpdateService(store, this.webSocketClient); |
31 | this.contentAssistService = new ContentAssistService(this.updateService); | 34 | this.contentAssistService = new ContentAssistService(this.updateService); |
35 | this.highlightingService = new HighlightingService(store, this.updateService); | ||
32 | this.validationService = new ValidationService(store, this.updateService); | 36 | this.validationService = new ValidationService(store, this.updateService); |
33 | } | 37 | } |
34 | 38 | ||
@@ -50,12 +54,12 @@ export class XtextClient { | |||
50 | await this.updateService.updateFullText(); | 54 | await this.updateService.updateFullText(); |
51 | } | 55 | } |
52 | switch (service) { | 56 | switch (service) { |
57 | case 'highlight': | ||
58 | this.highlightingService.onPush(push); | ||
59 | return; | ||
53 | case 'validate': | 60 | case 'validate': |
54 | this.validationService.onPush(push); | 61 | this.validationService.onPush(push); |
55 | return; | 62 | return; |
56 | case 'highlight': | ||
57 | // TODO | ||
58 | return; | ||
59 | default: | 63 | default: |
60 | log.error('Unknown push service:', service); | 64 | log.error('Unknown push service:', service); |
61 | break; | 65 | break; |
diff --git a/language-web/src/main/js/xtext/xtextServiceResults.ts b/language-web/src/main/js/xtext/xtextServiceResults.ts index 6c3d9daf..e32d49c3 100644 --- a/language-web/src/main/js/xtext/xtextServiceResults.ts +++ b/language-web/src/main/js/xtext/xtextServiceResults.ts | |||
@@ -198,3 +198,29 @@ export function isContentAssistResult(result: unknown): result is IContentAssist | |||
198 | return isDocumentStateResult(result) | 198 | return isDocumentStateResult(result) |
199 | && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); | 199 | && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); |
200 | } | 200 | } |
201 | |||
202 | export interface IHighlightingRegion { | ||
203 | offset: number; | ||
204 | |||
205 | length: number; | ||
206 | |||
207 | styleClasses: string[]; | ||
208 | } | ||
209 | |||
210 | export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { | ||
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 | |||
218 | export interface IHighlightingResult { | ||
219 | regions: IHighlightingRegion[]; | ||
220 | } | ||
221 | |||
222 | export function isHighlightingResult(result: unknown): result is IHighlightingResult { | ||
223 | const highlightingResult = result as IHighlightingResult; | ||
224 | return typeof highlightingResult === 'object' | ||
225 | && isArrayOfType(highlightingResult.regions, isHighlightingRegion); | ||
226 | } | ||
diff --git a/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java index 5ccd155f..d42cc15c 100644 --- a/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java +++ b/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java | |||
@@ -96,7 +96,7 @@ class ProblemWebSocketServletIntegrationTest { | |||
96 | case 0 -> session.getRemote().sendString( | 96 | case 0 -> session.getRemote().sendString( |
97 | "{\"id\":\"foo\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"fullText\":\"class Person.\n\"}}"); | 97 | "{\"id\":\"foo\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"fullText\":\"class Person.\n\"}}"); |
98 | case 3 -> session.getRemote().sendString( | 98 | case 3 -> session.getRemote().sendString( |
99 | "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"requiredStateId\":\"-80000000\",\"deltaText\":\"class Car.\n\",\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}"); | 99 | "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"requiredStateId\":\"-80000000\",\"deltaText\":\"unique q.\nnode(q).\n\",\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}"); |
100 | case 5 -> session.close(); | 100 | case 5 -> session.close(); |
101 | } | 101 | } |
102 | } | 102 | } |
diff --git a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java index 0892954b..975d120c 100644 --- a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java +++ b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java | |||
@@ -79,7 +79,7 @@ class TransactionExecutorTest { | |||
79 | var stateId = updateFullText(); | 79 | var stateId = updateFullText(); |
80 | var responseHandler = sendRequestAndWaitForAllResponses( | 80 | var responseHandler = sendRequestAndWaitForAllResponses( |
81 | new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId", | 81 | new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId", |
82 | stateId, "deltaText", "<invalid text>\n", "deltaOffset", "0", "deltaReplaceLength", "0"))); | 82 | stateId, "deltaText", "unique q.\nnode(q).\n<invalid text>\n", "deltaOffset", "0", "deltaReplaceLength", "0"))); |
83 | 83 | ||
84 | var captor = newCaptor(); | 84 | var captor = newCaptor(); |
85 | verify(responseHandler, times(3)).onResponse(captor.capture()); | 85 | verify(responseHandler, times(3)).onResponse(captor.capture()); |
@@ -92,7 +92,7 @@ class TransactionExecutorTest { | |||
92 | var stateId = updateFullText(); | 92 | var stateId = updateFullText(); |
93 | var responseHandler = sendRequestAndWaitForAllResponses( | 93 | var responseHandler = sendRequestAndWaitForAllResponses( |
94 | new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId", | 94 | new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId", |
95 | stateId, "deltaText", "class Vehicle.\n", "deltaOffset", "0", "deltaReplaceLength", "0"))); | 95 | stateId, "deltaText", "unique q.\nnode(q).\n", "deltaOffset", "0", "deltaReplaceLength", "0"))); |
96 | 96 | ||
97 | var captor = newCaptor(); | 97 | var captor = newCaptor(); |
98 | verify(responseHandler, times(2)).onResponse(captor.capture()); | 98 | verify(responseHandler, times(2)).onResponse(captor.capture()); |
diff --git a/language-web/yarn.lock b/language-web/yarn.lock index a29eb715..7c00795a 100644 --- a/language-web/yarn.lock +++ b/language-web/yarn.lock | |||
@@ -1096,7 +1096,7 @@ | |||
1096 | "@codemirror/state" "^0.19.0" | 1096 | "@codemirror/state" "^0.19.0" |
1097 | "@codemirror/view" "^0.19.0" | 1097 | "@codemirror/view" "^0.19.0" |
1098 | 1098 | ||
1099 | "@codemirror/rangeset@^0.19.0": | 1099 | "@codemirror/rangeset@^0.19.0", "@codemirror/rangeset@^0.19.1": |
1100 | version "0.19.1" | 1100 | version "0.19.1" |
1101 | resolved "https://registry.yarnpkg.com/@codemirror/rangeset/-/rangeset-0.19.1.tgz#03ab6f93fb60d9ba98f810b98ed9471cba1e3854" | 1101 | resolved "https://registry.yarnpkg.com/@codemirror/rangeset/-/rangeset-0.19.1.tgz#03ab6f93fb60d9ba98f810b98ed9471cba1e3854" |
1102 | integrity sha512-WaKTEw8JB/3QFlQzpdgRoklopcWvG8O/Xp+rxxOfFKYTaeaejpY/tjpyBBg+Ea65Ka3m7+pPp9d5j/oR2rd9NA== | 1102 | integrity sha512-WaKTEw8JB/3QFlQzpdgRoklopcWvG8O/Xp+rxxOfFKYTaeaejpY/tjpyBBg+Ea65Ka3m7+pPp9d5j/oR2rd9NA== |