aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-11-25 15:28:56 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-12-09 00:07:38 +0100
commitcc27b2fbea058022f2f3fc3b3f74d91355d7e237 (patch)
tree7b81505b181c821f66aa629f7702b975bd9eb2b3
parentchore(web): fix lint error (diff)
downloadrefinery-cc27b2fbea058022f2f3fc3b3f74d91355d7e237.tar.gz
refinery-cc27b2fbea058022f2f3fc3b3f74d91355d7e237.tar.zst
refinery-cc27b2fbea058022f2f3fc3b3f74d91355d7e237.zip
refactor(frontend): simplify diagnostic tracking
-rw-r--r--subprojects/frontend/src/editor/DiagnosticValue.ts6
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts26
-rw-r--r--subprojects/frontend/src/editor/createEditorState.ts2
-rw-r--r--subprojects/frontend/src/editor/exposeDiagnostics.ts80
-rw-r--r--subprojects/frontend/src/editor/scrollbarViewPlugin.ts22
5 files changed, 98 insertions, 38 deletions
diff --git a/subprojects/frontend/src/editor/DiagnosticValue.ts b/subprojects/frontend/src/editor/DiagnosticValue.ts
index ad23c467..b4e0b165 100644
--- a/subprojects/frontend/src/editor/DiagnosticValue.ts
+++ b/subprojects/frontend/src/editor/DiagnosticValue.ts
@@ -1,14 +1,16 @@
1import type { Diagnostic } from '@codemirror/lint'; 1import type { Diagnostic } from '@codemirror/lint';
2import { RangeValue } from '@codemirror/state'; 2import { RangeValue } from '@codemirror/state';
3 3
4export type Severity = Diagnostic['severity'];
5
4export default class DiagnosticValue extends RangeValue { 6export default class DiagnosticValue extends RangeValue {
5 static VALUES: Record<Diagnostic['severity'], DiagnosticValue> = { 7 static VALUES: Record<Severity, DiagnosticValue> = {
6 error: new DiagnosticValue('error'), 8 error: new DiagnosticValue('error'),
7 warning: new DiagnosticValue('warning'), 9 warning: new DiagnosticValue('warning'),
8 info: new DiagnosticValue('info'), 10 info: new DiagnosticValue('info'),
9 }; 11 };
10 12
11 private constructor(public readonly severity: Diagnostic['severity']) { 13 private constructor(public readonly severity: Severity) {
12 super(); 14 super();
13 } 15 }
14 16
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index d966690c..1f301c31 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -14,7 +14,6 @@ import {
14 type Transaction, 14 type Transaction,
15 type TransactionSpec, 15 type TransactionSpec,
16 type EditorState, 16 type EditorState,
17 RangeSet,
18} from '@codemirror/state'; 17} from '@codemirror/state';
19import { type Command, EditorView } from '@codemirror/view'; 18import { type Command, EditorView } from '@codemirror/view';
20import { makeAutoObservable, observable } from 'mobx'; 19import { makeAutoObservable, observable } from 'mobx';
@@ -24,10 +23,10 @@ import type PWAStore from '../PWAStore';
24import getLogger from '../utils/getLogger'; 23import getLogger from '../utils/getLogger';
25import XtextClient from '../xtext/XtextClient'; 24import XtextClient from '../xtext/XtextClient';
26 25
27import DiagnosticValue from './DiagnosticValue';
28import LintPanelStore from './LintPanelStore'; 26import LintPanelStore from './LintPanelStore';
29import SearchPanelStore from './SearchPanelStore'; 27import SearchPanelStore from './SearchPanelStore';
30import createEditorState from './createEditorState'; 28import createEditorState from './createEditorState';
29import { countDiagnostics } from './exposeDiagnostics';
31import { type IOccurrence, setOccurrences } from './findOccurrences'; 30import { type IOccurrence, setOccurrences } from './findOccurrences';
32import { 31import {
33 type IHighlightRange, 32 type IHighlightRange,
@@ -51,8 +50,6 @@ export default class EditorStore {
51 50
52 showLineNumbers = false; 51 showLineNumbers = false;
53 52
54 diagnostics: RangeSet<DiagnosticValue> = RangeSet.of([]);
55
56 constructor(initialValue: string, pwaStore: PWAStore) { 53 constructor(initialValue: string, pwaStore: PWAStore) {
57 this.id = nanoid(); 54 this.id = nanoid();
58 this.state = createEditorState(initialValue, this); 55 this.state = createEditorState(initialValue, this);
@@ -162,9 +159,6 @@ export default class EditorStore {
162 log.trace('Editor transaction', tr); 159 log.trace('Editor transaction', tr);
163 this.state = tr.state; 160 this.state = tr.state;
164 this.client.onTransaction(tr); 161 this.client.onTransaction(tr);
165 if (tr.docChanged) {
166 this.diagnostics = this.diagnostics.map(tr.changes);
167 }
168 } 162 }
169 163
170 doCommand(command: Command): boolean { 164 doCommand(command: Command): boolean {
@@ -182,31 +176,19 @@ export default class EditorStore {
182 } 176 }
183 177
184 updateDiagnostics(diagnostics: Diagnostic[]): void { 178 updateDiagnostics(diagnostics: Diagnostic[]): void {
185 diagnostics.sort((a, b) => a.from - b.from);
186 this.dispatch(setDiagnostics(this.state, diagnostics)); 179 this.dispatch(setDiagnostics(this.state, diagnostics));
187 this.diagnostics = RangeSet.of(
188 diagnostics.map(({ severity, from, to }) =>
189 DiagnosticValue.VALUES[severity].range(from, to),
190 ),
191 );
192 }
193
194 countDiagnostics(severity: Diagnostic['severity']): number {
195 return this.diagnostics.update({
196 filter: (_from, _to, value) => value.eq(DiagnosticValue.VALUES[severity]),
197 }).size;
198 } 180 }
199 181
200 get errorCount(): number { 182 get errorCount(): number {
201 return this.countDiagnostics('error'); 183 return countDiagnostics(this.state, 'error');
202 } 184 }
203 185
204 get warningCount(): number { 186 get warningCount(): number {
205 return this.countDiagnostics('warning'); 187 return countDiagnostics(this.state, 'warning');
206 } 188 }
207 189
208 get infoCount(): number { 190 get infoCount(): number {
209 return this.countDiagnostics('info'); 191 return countDiagnostics(this.state, 'info');
210 } 192 }
211 193
212 nextDiagnostic(): void { 194 nextDiagnostic(): void {
diff --git a/subprojects/frontend/src/editor/createEditorState.ts b/subprojects/frontend/src/editor/createEditorState.ts
index 05028fcc..ce1efa4f 100644
--- a/subprojects/frontend/src/editor/createEditorState.ts
+++ b/subprojects/frontend/src/editor/createEditorState.ts
@@ -36,6 +36,7 @@ import problemLanguageSupport from '../language/problemLanguageSupport';
36 36
37import type EditorStore from './EditorStore'; 37import type EditorStore from './EditorStore';
38import SearchPanel from './SearchPanel'; 38import SearchPanel from './SearchPanel';
39import exposeDiagnostics from './exposeDiagnostics';
39import findOccurrences from './findOccurrences'; 40import findOccurrences from './findOccurrences';
40import indentationMarkerViewPlugin from './indentationMarkerViewPlugin'; 41import indentationMarkerViewPlugin from './indentationMarkerViewPlugin';
41import scrollbarViewPlugin from './scrollbarViewPlugin'; 42import scrollbarViewPlugin from './scrollbarViewPlugin';
@@ -56,6 +57,7 @@ export default function createEditorState(
56 bracketMatching(), 57 bracketMatching(),
57 drawSelection(), 58 drawSelection(),
58 EditorState.allowMultipleSelections.of(true), 59 EditorState.allowMultipleSelections.of(true),
60 exposeDiagnostics,
59 findOccurrences, 61 findOccurrences,
60 highlightActiveLine(), 62 highlightActiveLine(),
61 highlightActiveLineGutter(), 63 highlightActiveLineGutter(),
diff --git a/subprojects/frontend/src/editor/exposeDiagnostics.ts b/subprojects/frontend/src/editor/exposeDiagnostics.ts
new file mode 100644
index 00000000..82f24c93
--- /dev/null
+++ b/subprojects/frontend/src/editor/exposeDiagnostics.ts
@@ -0,0 +1,80 @@
1import { setDiagnosticsEffect } from '@codemirror/lint';
2import {
3 StateField,
4 RangeSet,
5 type Extension,
6 type EditorState,
7} from '@codemirror/state';
8
9import DiagnosticValue, { type Severity } from './DiagnosticValue';
10
11type SeverityCounts = Partial<Record<Severity, number>>;
12
13interface ExposedDiagnostics {
14 readonly diagnostics: RangeSet<DiagnosticValue>;
15
16 readonly severityCounts: SeverityCounts;
17}
18
19function countSeverities(
20 diagnostics: RangeSet<DiagnosticValue>,
21): SeverityCounts {
22 const severityCounts: SeverityCounts = {};
23 const iter = diagnostics.iter();
24 while (iter.value !== null) {
25 const {
26 value: { severity },
27 } = iter;
28 severityCounts[severity] = (severityCounts[severity] ?? 0) + 1;
29 iter.next();
30 }
31 return severityCounts;
32}
33
34const exposedDiagnosticsState = StateField.define<ExposedDiagnostics>({
35 create() {
36 return {
37 diagnostics: RangeSet.of([]),
38 severityCounts: {},
39 };
40 },
41
42 update({ diagnostics: diagnosticsSet, severityCounts }, transaction) {
43 let newDiagnosticsSet = diagnosticsSet;
44 if (transaction.docChanged) {
45 newDiagnosticsSet = newDiagnosticsSet.map(transaction.changes);
46 }
47 transaction.effects.forEach((effect) => {
48 if (effect.is(setDiagnosticsEffect)) {
49 const diagnostics = effect.value.map(({ severity, from, to }) =>
50 DiagnosticValue.VALUES[severity].range(from, to),
51 );
52 diagnostics.sort(({ from: a }, { from: b }) => a - b);
53 newDiagnosticsSet = RangeSet.of(diagnostics);
54 }
55 });
56 return {
57 diagnostics: newDiagnosticsSet,
58 severityCounts:
59 // Only recompute if the diagnostics were changed.
60 diagnosticsSet === newDiagnosticsSet
61 ? severityCounts
62 : countSeverities(newDiagnosticsSet),
63 };
64 },
65});
66
67const exposeDiagnostics: Extension = [exposedDiagnosticsState];
68
69export default exposeDiagnostics;
70
71export function getDiagnostics(state: EditorState): RangeSet<DiagnosticValue> {
72 return state.field(exposedDiagnosticsState).diagnostics;
73}
74
75export function countDiagnostics(
76 state: EditorState,
77 severity: Severity,
78): number {
79 return state.field(exposedDiagnosticsState).severityCounts[severity] ?? 0;
80}
diff --git a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts
index 0edaeb70..b0ea769f 100644
--- a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts
+++ b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts
@@ -2,6 +2,7 @@ import { type PluginValue, ViewPlugin } from '@codemirror/view';
2import { reaction } from 'mobx'; 2import { reaction } from 'mobx';
3 3
4import type EditorStore from './EditorStore'; 4import type EditorStore from './EditorStore';
5import { getDiagnostics } from './exposeDiagnostics';
5import findOccurrences from './findOccurrences'; 6import findOccurrences from './findOccurrences';
6 7
7export const HOLDER_CLASS = 'cm-scroller-holder'; 8export const HOLDER_CLASS = 'cm-scroller-holder';
@@ -105,11 +106,12 @@ export default function scrollbarViewPlugin(
105 const annotations: HTMLDivElement[] = []; 106 const annotations: HTMLDivElement[] = [];
106 107
107 function rebuildAnnotations(trackYHeight: number) { 108 function rebuildAnnotations(trackYHeight: number) {
109 const { state } = view;
108 const annotationOverlayHeight = Math.min( 110 const annotationOverlayHeight = Math.min(
109 view.contentHeight, 111 view.contentHeight,
110 trackYHeight, 112 trackYHeight,
111 ); 113 );
112 const lineHeight = annotationOverlayHeight / editorStore.state.doc.lines; 114 const lineHeight = annotationOverlayHeight / state.doc.lines;
113 115
114 let i = 0; 116 let i = 0;
115 117
@@ -117,11 +119,9 @@ export default function scrollbarViewPlugin(
117 from: number, 119 from: number,
118 to?: number, 120 to?: number,
119 ): HTMLDivElement { 121 ): HTMLDivElement {
120 const startLine = editorStore.state.doc.lineAt(from).number; 122 const startLine = state.doc.lineAt(from).number;
121 const endLine = 123 const endLine =
122 to === undefined 124 to === undefined ? startLine : state.doc.lineAt(to).number;
123 ? startLine
124 : editorStore.state.doc.lineAt(to).number;
125 const top = (startLine - 1) * lineHeight; 125 const top = (startLine - 1) * lineHeight;
126 const height = Math.max( 126 const height = Math.max(
127 MIN_ANNOTATION_HEIGHT, 127 MIN_ANNOTATION_HEIGHT,
@@ -145,13 +145,13 @@ export default function scrollbarViewPlugin(
145 return annotation; 145 return annotation;
146 } 146 }
147 147
148 editorStore.state.selection.ranges.forEach(({ head }) => { 148 state.selection.ranges.forEach(({ head }) => {
149 const selectionAnnotation = getOrCreateAnnotation(head); 149 const selectionAnnotation = getOrCreateAnnotation(head);
150 selectionAnnotation.className = ANNOTATION_SELECTION_CLASS; 150 selectionAnnotation.className = ANNOTATION_SELECTION_CLASS;
151 selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`; 151 selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`;
152 }); 152 });
153 153
154 const diagnosticsIter = editorStore.diagnostics.iter(); 154 const diagnosticsIter = getDiagnostics(state).iter();
155 while (diagnosticsIter.value !== null) { 155 while (diagnosticsIter.value !== null) {
156 const diagnosticAnnotation = getOrCreateAnnotation( 156 const diagnosticAnnotation = getOrCreateAnnotation(
157 diagnosticsIter.from, 157 diagnosticsIter.from,
@@ -162,7 +162,7 @@ export default function scrollbarViewPlugin(
162 diagnosticsIter.next(); 162 diagnosticsIter.next();
163 } 163 }
164 164
165 const occurrences = editorStore.state.field(findOccurrences); 165 const occurrences = view.state.field(findOccurrences);
166 const occurrencesIter = occurrences.iter(); 166 const occurrencesIter = occurrences.iter();
167 while (occurrencesIter.value !== null) { 167 while (occurrencesIter.value !== null) {
168 const occurrenceAnnotation = getOrCreateAnnotation( 168 const occurrenceAnnotation = getOrCreateAnnotation(
@@ -259,15 +259,9 @@ export default function scrollbarViewPlugin(
259 259
260 requestRebuild(); 260 requestRebuild();
261 261
262 const disposeRebuildReaction = reaction(
263 () => editorStore.diagnostics,
264 requestRebuild,
265 );
266
267 return { 262 return {
268 update: requestRebuild, 263 update: requestRebuild,
269 destroy() { 264 destroy() {
270 disposeRebuildReaction();
271 disposePanelReaction(); 265 disposePanelReaction();
272 observer?.disconnect(); 266 observer?.disconnect();
273 scrollDOM.removeEventListener('scroll', requestUpdate); 267 scrollDOM.removeEventListener('scroll', requestUpdate);