From cbf442d8fd9f72c567ebf9f036a219a9ff100487 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 30 Oct 2021 20:14:50 +0200 Subject: feat(web): semantic highlighting --- language-web/src/main/js/editor/EditorParent.ts | 28 ++++++++++++++ language-web/src/main/js/editor/EditorStore.ts | 9 ++++- .../src/main/js/editor/semanticHighlighting.ts | 34 +++++++++++++++++ language-web/src/main/js/index.tsx | 2 +- .../src/main/js/xtext/HighlightingService.ts | 43 ++++++++++++++++++++++ .../src/main/js/xtext/ValidationService.ts | 17 ++++++--- language-web/src/main/js/xtext/XtextClient.ts | 10 +++-- .../src/main/js/xtext/xtextServiceResults.ts | 26 +++++++++++++ 8 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 language-web/src/main/js/editor/semanticHighlighting.ts create mode 100644 language-web/src/main/js/xtext/HighlightingService.ts (limited to 'language-web/src/main/js') 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 }) => { '.cmt-variableName': { color: '#c8ae9d', }, + '.cmt-problem-node': { + '&, & .cmt-variableName': { + color: theme.palette.text.secondary, + }, + }, + '.cmt-problem-unique': { + '&, & .cmt-variableName': { + color: theme.palette.text.primary, + }, + }, + '.cmt-problem-abstract, .cmt-problem-new': { + fontStyle: 'italic', + }, + '.cmt-problem-containment': { + fontWeight: 700, + }, + '.cmt-problem-error': { + '&, & .cmt-typeName': { + color: theme.palette.error.main, + }, + }, + '.cmt-problem-builtin': { + '&, & .cmt-typeName, & .cmt-atom, & .cmt-variableName': { + color: theme.palette.primary.main, + fontWeight: 400, + fontStyle: 'normal', + }, + }, '.cm-tooltip-autocomplete': { background: theme.palette.background.paper, 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 { TransactionSpec, } from '@codemirror/state'; import { + DecorationSet, drawSelection, EditorView, highlightActiveLine, @@ -43,8 +44,9 @@ import { } from 'mobx'; import { problemLanguageSupport } from '../language/problemLanguageSupport'; -import { getLogger } from '../utils/logger'; +import { semanticHighlighting, setSemanticHighlighting } from './semanticHighlighting'; import type { ThemeStore } from '../theme/ThemeStore'; +import { getLogger } from '../utils/logger'; import { XtextClient } from '../xtext/XtextClient'; const log = getLogger('editor.EditorStore'); @@ -103,6 +105,7 @@ export class EditorStore { top: true, matchCase: true, }), + semanticHighlighting, // We add the gutters to `extensions` in the order we want them to appear. foldGutter(), lineNumbers(), @@ -201,6 +204,10 @@ export class EditorStore { return null; } + updateSemanticHighlighting(decorations: DecorationSet): void { + this.dispatch(setSemanticHighlighting(decorations)); + } + /** * @returns `true` if there is history to undo */ 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 @@ +import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'; +import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; + +const setSemanticHighlightingEffect = StateEffect.define(); + +export function setSemanticHighlighting(decorations: DecorationSet): TransactionSpec { + return { + effects: [ + setSemanticHighlightingEffect.of(decorations), + ], + }; +} + +export const semanticHighlighting = StateField.define({ + create() { + return Decoration.none; + }, + update(currentDecorations, transaction) { + let newDecorations: DecorationSet | null = null; + transaction.effects.forEach((effect) => { + if (effect.is(setSemanticHighlightingEffect)) { + newDecorations = effect.value; + } + }); + if (newDecorations === null) { + if (transaction.docChanged) { + return currentDecorations.map(transaction.changes); + } + return currentDecorations; + } + return newDecorations; + }, + provide: (f) => EditorView.decorations.from(f), +}); 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 { } % A child cannot have any dependents. -error invalidTaxStatus(Person p) <-> +pred invalidTaxStatus(Person p) <-> taxStatus(p, child), children(p, _q) ; 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 @@ +import { Decoration } from '@codemirror/view'; +import { Range, RangeSet } from '@codemirror/rangeset'; + +import type { EditorStore } from '../editor/EditorStore'; +import type { UpdateService } from './UpdateService'; +import { getLogger } from '../utils/logger'; +import { isHighlightingResult } from './xtextServiceResults'; + +const log = getLogger('xtext.ValidationService'); + +export class HighlightingService { + private store: EditorStore; + + private updateService: UpdateService; + + constructor(store: EditorStore, updateService: UpdateService) { + this.store = store; + this.updateService = updateService; + } + + onPush(push: unknown): void { + if (!isHighlightingResult(push)) { + log.error('Invalid highlighting result', push); + return; + } + const allChanges = this.updateService.computeChangesSinceLastUpdate(); + const decorations: Range[] = []; + push.regions.forEach(({ offset, length, styleClasses }) => { + if (styleClasses.length === 0) { + return; + } + const from = allChanges.mapPos(offset); + const to = allChanges.mapPos(offset + length); + if (to <= from) { + return; + } + decorations.push(Decoration.mark({ + class: styleClasses.map((c) => `cmt-problem-${c}`).join(' '), + }).range(from, to)); + }); + this.store.updateSemanticHighlighting(RangeSet.of(decorations, true)); + } +} 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 { } const allChanges = this.updateService.computeChangesSinceLastUpdate(); const diagnostics: Diagnostic[] = []; - push.issues.forEach((issue) => { - if (issue.severity === 'ignore') { + push.issues.forEach(({ + offset, + length, + severity, + description, + }) => { + if (severity === 'ignore') { return; } diagnostics.push({ - from: allChanges.mapPos(issue.offset), - to: allChanges.mapPos(issue.offset + issue.length), - severity: issue.severity, - message: issue.description, + from: allChanges.mapPos(offset), + to: allChanges.mapPos(offset + length), + severity, + message: description, }); }); 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'; import type { EditorStore } from '../editor/EditorStore'; import { ContentAssistService } from './ContentAssistService'; +import { HighlightingService } from './HighlightingService'; import { UpdateService } from './UpdateService'; import { getLogger } from '../utils/logger'; import { ValidationService } from './ValidationService'; @@ -20,6 +21,8 @@ export class XtextClient { private contentAssistService: ContentAssistService; + private highlightingService: HighlightingService; + private validationService: ValidationService; constructor(store: EditorStore) { @@ -29,6 +32,7 @@ export class XtextClient { ); this.updateService = new UpdateService(store, this.webSocketClient); this.contentAssistService = new ContentAssistService(this.updateService); + this.highlightingService = new HighlightingService(store, this.updateService); this.validationService = new ValidationService(store, this.updateService); } @@ -50,12 +54,12 @@ export class XtextClient { await this.updateService.updateFullText(); } switch (service) { + case 'highlight': + this.highlightingService.onPush(push); + return; case 'validate': this.validationService.onPush(push); return; - case 'highlight': - // TODO - return; default: log.error('Unknown push service:', service); 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 return isDocumentStateResult(result) && isArrayOfType(contentAssistResult.entries, isContentAssistEntry); } + +export interface IHighlightingRegion { + offset: number; + + length: number; + + styleClasses: string[]; +} + +export function isHighlightingRegion(value: unknown): value is IHighlightingRegion { + const region = value as IHighlightingRegion; + return typeof region === 'object' + && typeof region.offset === 'number' + && typeof region.length === 'number' + && isArrayOfType(region.styleClasses, (s): s is string => typeof s === 'string'); +} + +export interface IHighlightingResult { + regions: IHighlightingRegion[]; +} + +export function isHighlightingResult(result: unknown): result is IHighlightingResult { + const highlightingResult = result as IHighlightingResult; + return typeof highlightingResult === 'object' + && isArrayOfType(highlightingResult.regions, isHighlightingRegion); +} -- cgit v1.2.3-54-g00ecf