diff options
Diffstat (limited to 'subprojects/frontend/src')
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 @@ | |||
1 | import type { Diagnostic } from '@codemirror/lint'; | 1 | import type { Diagnostic } from '@codemirror/lint'; |
2 | import { RangeValue } from '@codemirror/state'; | 2 | import { RangeValue } from '@codemirror/state'; |
3 | 3 | ||
4 | export type Severity = Diagnostic['severity']; | ||
5 | |||
4 | export default class DiagnosticValue extends RangeValue { | 6 | export 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'; |
19 | import { type Command, EditorView } from '@codemirror/view'; | 18 | import { type Command, EditorView } from '@codemirror/view'; |
20 | import { makeAutoObservable, observable } from 'mobx'; | 19 | import { makeAutoObservable, observable } from 'mobx'; |
@@ -24,10 +23,10 @@ import type PWAStore from '../PWAStore'; | |||
24 | import getLogger from '../utils/getLogger'; | 23 | import getLogger from '../utils/getLogger'; |
25 | import XtextClient from '../xtext/XtextClient'; | 24 | import XtextClient from '../xtext/XtextClient'; |
26 | 25 | ||
27 | import DiagnosticValue from './DiagnosticValue'; | ||
28 | import LintPanelStore from './LintPanelStore'; | 26 | import LintPanelStore from './LintPanelStore'; |
29 | import SearchPanelStore from './SearchPanelStore'; | 27 | import SearchPanelStore from './SearchPanelStore'; |
30 | import createEditorState from './createEditorState'; | 28 | import createEditorState from './createEditorState'; |
29 | import { countDiagnostics } from './exposeDiagnostics'; | ||
31 | import { type IOccurrence, setOccurrences } from './findOccurrences'; | 30 | import { type IOccurrence, setOccurrences } from './findOccurrences'; |
32 | import { | 31 | import { |
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 | ||
37 | import type EditorStore from './EditorStore'; | 37 | import type EditorStore from './EditorStore'; |
38 | import SearchPanel from './SearchPanel'; | 38 | import SearchPanel from './SearchPanel'; |
39 | import exposeDiagnostics from './exposeDiagnostics'; | ||
39 | import findOccurrences from './findOccurrences'; | 40 | import findOccurrences from './findOccurrences'; |
40 | import indentationMarkerViewPlugin from './indentationMarkerViewPlugin'; | 41 | import indentationMarkerViewPlugin from './indentationMarkerViewPlugin'; |
41 | import scrollbarViewPlugin from './scrollbarViewPlugin'; | 42 | import 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 @@ | |||
1 | import { setDiagnosticsEffect } from '@codemirror/lint'; | ||
2 | import { | ||
3 | StateField, | ||
4 | RangeSet, | ||
5 | type Extension, | ||
6 | type EditorState, | ||
7 | } from '@codemirror/state'; | ||
8 | |||
9 | import DiagnosticValue, { type Severity } from './DiagnosticValue'; | ||
10 | |||
11 | type SeverityCounts = Partial<Record<Severity, number>>; | ||
12 | |||
13 | interface ExposedDiagnostics { | ||
14 | readonly diagnostics: RangeSet<DiagnosticValue>; | ||
15 | |||
16 | readonly severityCounts: SeverityCounts; | ||
17 | } | ||
18 | |||
19 | function 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 | |||
34 | const 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 | |||
67 | const exposeDiagnostics: Extension = [exposedDiagnosticsState]; | ||
68 | |||
69 | export default exposeDiagnostics; | ||
70 | |||
71 | export function getDiagnostics(state: EditorState): RangeSet<DiagnosticValue> { | ||
72 | return state.field(exposedDiagnosticsState).diagnostics; | ||
73 | } | ||
74 | |||
75 | export 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'; | |||
2 | import { reaction } from 'mobx'; | 2 | import { reaction } from 'mobx'; |
3 | 3 | ||
4 | import type EditorStore from './EditorStore'; | 4 | import type EditorStore from './EditorStore'; |
5 | import { getDiagnostics } from './exposeDiagnostics'; | ||
5 | import findOccurrences from './findOccurrences'; | 6 | import findOccurrences from './findOccurrences'; |
6 | 7 | ||
7 | export const HOLDER_CLASS = 'cm-scroller-holder'; | 8 | export 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); |