diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-11-01 12:05:21 -0400 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-11-05 19:41:33 +0100 |
commit | ffe5b08149681f51625bdac43c7ab41ccf5f9cc3 (patch) | |
tree | f19137f20c73613ed8d4e63a725e4a1d20a00ec2 /subprojects/frontend/src/editor/scrollbarViewPlugin.ts | |
parent | feat(frontend): overlay scrollbars for editor (diff) | |
download | refinery-ffe5b08149681f51625bdac43c7ab41ccf5f9cc3.tar.gz refinery-ffe5b08149681f51625bdac43c7ab41ccf5f9cc3.tar.zst refinery-ffe5b08149681f51625bdac43c7ab41ccf5f9cc3.zip |
feat(frontend): scrollbar annotations
Diffstat (limited to 'subprojects/frontend/src/editor/scrollbarViewPlugin.ts')
-rw-r--r-- | subprojects/frontend/src/editor/scrollbarViewPlugin.ts | 106 |
1 files changed, 103 insertions, 3 deletions
diff --git a/subprojects/frontend/src/editor/scrollbarViewPlugin.ts b/subprojects/frontend/src/editor/scrollbarViewPlugin.ts index 2882f02e..c95e581d 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 findOccurrences from './findOccurrences'; | ||
5 | 6 | ||
6 | export const HOLDER_CLASS = 'cm-scroller-holder'; | 7 | export const HOLDER_CLASS = 'cm-scroller-holder'; |
7 | export const THUMB_CLASS = 'cm-scroller-thumb'; | 8 | export const THUMB_CLASS = 'cm-scroller-thumb'; |
@@ -10,8 +11,13 @@ export const THUMB_X_CLASS = 'cm-scroller-thumb-x'; | |||
10 | export const THUMB_ACTIVE_CLASS = 'active'; | 11 | export const THUMB_ACTIVE_CLASS = 'active'; |
11 | export const GUTTER_DECORATION_CLASS = 'cm-scroller-gutter-decoration'; | 12 | export const GUTTER_DECORATION_CLASS = 'cm-scroller-gutter-decoration'; |
12 | export const TOP_DECORATION_CLASS = 'cm-scroller-top-decoration'; | 13 | export const TOP_DECORATION_CLASS = 'cm-scroller-top-decoration'; |
14 | export const ANNOTATION_SELECTION_CLASS = 'cm-scroller-selection'; | ||
15 | export const ANNOTATION_DIAGNOSTIC_CLASS = 'cm-scroller-diagnostic'; | ||
16 | export const ANNOTATION_OCCURRENCE_CLASS = 'cm-scroller-occurrence'; | ||
13 | export const SHADOW_WIDTH = 10; | 17 | export const SHADOW_WIDTH = 10; |
14 | export const SCROLLBAR_WIDTH = 12; | 18 | export const SCROLLBAR_WIDTH = 12; |
19 | export const ANNOTATION_WIDTH = SCROLLBAR_WIDTH / 2; | ||
20 | export const MIN_ANNOTATION_HEIGHT = 1; | ||
15 | 21 | ||
16 | export default function scrollbarViewPlugin( | 22 | export default function scrollbarViewPlugin( |
17 | editorStore: EditorStore, | 23 | editorStore: EditorStore, |
@@ -94,6 +100,84 @@ export default function scrollbarViewPlugin( | |||
94 | let gutters: Element | undefined; | 100 | let gutters: Element | undefined; |
95 | 101 | ||
96 | let requested = false; | 102 | let requested = false; |
103 | let rebuildRequested = false; | ||
104 | |||
105 | const annotations: HTMLDivElement[] = []; | ||
106 | |||
107 | function rebuildAnnotations(trackYHeight: number) { | ||
108 | const annotationOverlayHeight = Math.min( | ||
109 | view.contentHeight, | ||
110 | trackYHeight, | ||
111 | ); | ||
112 | const lineHeight = annotationOverlayHeight / editorStore.state.doc.lines; | ||
113 | |||
114 | let i = 0; | ||
115 | |||
116 | function getOrCreateAnnotation( | ||
117 | from: number, | ||
118 | to?: number, | ||
119 | ): HTMLDivElement { | ||
120 | const startLine = editorStore.state.doc.lineAt(from).number; | ||
121 | const endLine = | ||
122 | to === undefined | ||
123 | ? startLine | ||
124 | : editorStore.state.doc.lineAt(to).number; | ||
125 | const top = (startLine - 1) * lineHeight; | ||
126 | const height = Math.max( | ||
127 | MIN_ANNOTATION_HEIGHT, | ||
128 | Math.max(1, endLine - startLine) * lineHeight, | ||
129 | ); | ||
130 | |||
131 | let annotation: HTMLDivElement; | ||
132 | if (i < annotations.length) { | ||
133 | annotation = annotations[i]; | ||
134 | } else { | ||
135 | annotation = ownerDocument.createElement('div'); | ||
136 | annotations.push(annotation); | ||
137 | holder.appendChild(annotation); | ||
138 | } | ||
139 | i += 1; | ||
140 | |||
141 | annotation.style.top = `${top}px`; | ||
142 | annotation.style.height = `${height}px`; | ||
143 | |||
144 | return annotation; | ||
145 | } | ||
146 | |||
147 | editorStore.state.selection.ranges.forEach(({ head }) => { | ||
148 | const selectionAnnotation = getOrCreateAnnotation(head); | ||
149 | selectionAnnotation.className = ANNOTATION_SELECTION_CLASS; | ||
150 | selectionAnnotation.style.width = `${SCROLLBAR_WIDTH}px`; | ||
151 | }); | ||
152 | |||
153 | const diagnosticsIter = editorStore.diagnostics.iter(); | ||
154 | while (diagnosticsIter.value !== null) { | ||
155 | const diagnosticAnnotation = getOrCreateAnnotation( | ||
156 | diagnosticsIter.from, | ||
157 | diagnosticsIter.to, | ||
158 | ); | ||
159 | diagnosticAnnotation.className = `${ANNOTATION_DIAGNOSTIC_CLASS} ${ANNOTATION_DIAGNOSTIC_CLASS}-${diagnosticsIter.value.severity}`; | ||
160 | diagnosticAnnotation.style.width = `${ANNOTATION_WIDTH}px`; | ||
161 | diagnosticsIter.next(); | ||
162 | } | ||
163 | |||
164 | const occurrences = editorStore.state.field(findOccurrences); | ||
165 | const occurrencesIter = occurrences.iter(); | ||
166 | while (occurrencesIter.value !== null) { | ||
167 | const occurrenceAnnotation = getOrCreateAnnotation( | ||
168 | occurrencesIter.from, | ||
169 | occurrencesIter.to, | ||
170 | ); | ||
171 | occurrenceAnnotation.className = ANNOTATION_OCCURRENCE_CLASS; | ||
172 | occurrenceAnnotation.style.width = `${ANNOTATION_WIDTH}px`; | ||
173 | occurrenceAnnotation.style.right = `${ANNOTATION_WIDTH}px`; | ||
174 | occurrencesIter.next(); | ||
175 | } | ||
176 | |||
177 | annotations | ||
178 | .splice(i) | ||
179 | .forEach((staleAnnotation) => holder.removeChild(staleAnnotation)); | ||
180 | } | ||
97 | 181 | ||
98 | function update() { | 182 | function update() { |
99 | requested = false; | 183 | requested = false; |
@@ -148,6 +232,11 @@ export default function scrollbarViewPlugin( | |||
148 | 0, | 232 | 0, |
149 | Math.min(scrollTop, SHADOW_WIDTH), | 233 | Math.min(scrollTop, SHADOW_WIDTH), |
150 | )}px`; | 234 | )}px`; |
235 | |||
236 | if (rebuildRequested) { | ||
237 | rebuildAnnotations(trackYHeight); | ||
238 | rebuildRequested = false; | ||
239 | } | ||
151 | } | 240 | } |
152 | 241 | ||
153 | function requestUpdate() { | 242 | function requestUpdate() { |
@@ -157,16 +246,27 @@ export default function scrollbarViewPlugin( | |||
157 | } | 246 | } |
158 | } | 247 | } |
159 | 248 | ||
160 | observer = new ResizeObserver(requestUpdate); | 249 | function requestRebuild() { |
250 | requestUpdate(); | ||
251 | rebuildRequested = true; | ||
252 | } | ||
253 | |||
254 | observer = new ResizeObserver(requestRebuild); | ||
161 | observer.observe(scrollDOM); | 255 | observer.observe(scrollDOM); |
162 | 256 | ||
163 | scrollDOM.addEventListener('scroll', requestUpdate); | 257 | scrollDOM.addEventListener('scroll', requestUpdate); |
164 | 258 | ||
165 | requestUpdate(); | 259 | requestRebuild(); |
260 | |||
261 | const disposeRebuildReaction = reaction( | ||
262 | () => editorStore.diagnostics, | ||
263 | requestRebuild, | ||
264 | ); | ||
166 | 265 | ||
167 | return { | 266 | return { |
168 | update: requestUpdate, | 267 | update: requestRebuild, |
169 | destroy() { | 268 | destroy() { |
269 | disposeRebuildReaction(); | ||
170 | disposePanelReaction(); | 270 | disposePanelReaction(); |
171 | observer?.disconnect(); | 271 | observer?.disconnect(); |
172 | scrollDOM.removeEventListener('scroll', requestUpdate); | 272 | scrollDOM.removeEventListener('scroll', requestUpdate); |