@batchfy/codemirror-minimap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/Config.d.ts +63 -0
- package/dist/Config.d.ts.map +1 -0
- package/dist/Gutters.d.ts +11 -0
- package/dist/Gutters.d.ts.map +1 -0
- package/dist/LinesState.d.ts +14 -0
- package/dist/LinesState.d.ts.map +1 -0
- package/dist/Overlay.d.ts +2 -0
- package/dist/Overlay.d.ts.map +1 -0
- package/dist/diagnostics.d.ts +29 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1163 -0
- package/dist/index.js.map +1 -0
- package/dist/linebasedstate.d.ts +9 -0
- package/dist/linebasedstate.d.ts.map +1 -0
- package/dist/selections.d.ts +20 -0
- package/dist/selections.d.ts.map +1 -0
- package/dist/text.d.ts +38 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +77 -0
- package/src/Config.ts +110 -0
- package/src/Gutters.ts +29 -0
- package/src/LinesState.ts +96 -0
- package/src/Overlay.ts +320 -0
- package/src/diagnostics.ts +171 -0
- package/src/index.ts +368 -0
- package/src/linebasedstate.ts +21 -0
- package/src/selections.ts +212 -0
- package/src/text.ts +465 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { foldEffect, foldedRanges, unfoldEffect } from "@codemirror/language";
|
|
2
|
+
import { StateField, EditorState, Transaction } from "@codemirror/state";
|
|
3
|
+
import { Config } from "./Config.js";
|
|
4
|
+
|
|
5
|
+
type Span = { from: number; to: number; folded: boolean };
|
|
6
|
+
type Line = Array<Span>;
|
|
7
|
+
type Lines = Array<Line>;
|
|
8
|
+
|
|
9
|
+
function computeLinesState(state: EditorState): Lines {
|
|
10
|
+
if (!state.facet(Config).enabled) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lines: Lines = [];
|
|
15
|
+
|
|
16
|
+
const lineCursor = state.doc.iterLines();
|
|
17
|
+
const foldedRangeCursor = foldedRanges(state).iter();
|
|
18
|
+
|
|
19
|
+
let textOffset = 0;
|
|
20
|
+
lineCursor.next();
|
|
21
|
+
|
|
22
|
+
while (!lineCursor.done) {
|
|
23
|
+
const lineText = lineCursor.value;
|
|
24
|
+
const from = textOffset;
|
|
25
|
+
const to = from + lineText.length;
|
|
26
|
+
|
|
27
|
+
// Iterate through folded ranges until we're at or past the current line
|
|
28
|
+
while (foldedRangeCursor.value && foldedRangeCursor.to < from) {
|
|
29
|
+
foldedRangeCursor.next();
|
|
30
|
+
}
|
|
31
|
+
const { from: foldFrom, to: foldTo } = foldedRangeCursor;
|
|
32
|
+
|
|
33
|
+
const lineStartInFold = from >= foldFrom && from < foldTo;
|
|
34
|
+
const lineEndsInFold = to > foldFrom && to <= foldTo;
|
|
35
|
+
|
|
36
|
+
if (lineStartInFold) {
|
|
37
|
+
const lastLine = lines.pop() ?? [];
|
|
38
|
+
const lastRange = lastLine.pop();
|
|
39
|
+
|
|
40
|
+
// If the last range is folded, we extend the folded range
|
|
41
|
+
if (lastRange && lastRange.folded) {
|
|
42
|
+
lastRange.to = foldTo;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If we popped the last range, add it back
|
|
46
|
+
if (lastRange) {
|
|
47
|
+
lastLine.push(lastRange);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If we didn't have a previous range, or the previous range wasn't folded add a new range
|
|
51
|
+
if (!lastRange || !lastRange.folded) {
|
|
52
|
+
lastLine.push({ from: foldFrom, to: foldTo, folded: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If the line doesn't end in a fold, we add another token for the unfolded section
|
|
56
|
+
if (!lineEndsInFold) {
|
|
57
|
+
lastLine.push({ from: foldTo, to, folded: false });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lines.push(lastLine);
|
|
61
|
+
} else if (lineEndsInFold) {
|
|
62
|
+
lines.push([
|
|
63
|
+
{ from, to: foldFrom, folded: false },
|
|
64
|
+
{ from: foldFrom, to: foldTo, folded: true },
|
|
65
|
+
]);
|
|
66
|
+
} else {
|
|
67
|
+
lines.push([{ from, to, folded: false }]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
textOffset = to + 1;
|
|
71
|
+
lineCursor.next();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const LinesState = StateField.define<Lines>({
|
|
78
|
+
create: (state) => computeLinesState(state),
|
|
79
|
+
update: (current, tr) => {
|
|
80
|
+
if (foldsChanged([tr]) || tr.docChanged) {
|
|
81
|
+
return computeLinesState(tr.state);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return current;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Returns if the folds have changed in this update */
|
|
89
|
+
function foldsChanged(transactions: readonly Transaction[]): boolean {
|
|
90
|
+
return transactions.some((tr) =>
|
|
91
|
+
tr.effects.some((ef) => ef.is(foldEffect) || ef.is(unfoldEffect)),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { foldsChanged, LinesState };
|
|
96
|
+
export type { Lines };
|
package/src/Overlay.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
2
|
+
import { Config, Scale } from "./Config.js";
|
|
3
|
+
import crelt from "crelt";
|
|
4
|
+
|
|
5
|
+
const Theme = EditorView.theme({
|
|
6
|
+
".cm-minimap-overlay-container": {
|
|
7
|
+
position: "absolute",
|
|
8
|
+
top: 0,
|
|
9
|
+
height: "100%",
|
|
10
|
+
width: "100%",
|
|
11
|
+
"&.cm-minimap-overlay-mouse-over": {
|
|
12
|
+
opacity: 0,
|
|
13
|
+
transition: "visibility 0s linear 300ms, opacity 300ms",
|
|
14
|
+
},
|
|
15
|
+
"&.cm-minimap-overlay-mouse-over:hover": {
|
|
16
|
+
opacity: 1,
|
|
17
|
+
transition: "visibility 0s linear 0ms, opacity 300ms",
|
|
18
|
+
},
|
|
19
|
+
"&.cm-minimap-overlay-off": {
|
|
20
|
+
display: "none",
|
|
21
|
+
},
|
|
22
|
+
"& .cm-minimap-overlay": {
|
|
23
|
+
background: "rgb(121, 121, 121)",
|
|
24
|
+
opacity: "0.2",
|
|
25
|
+
position: "absolute",
|
|
26
|
+
right: 0,
|
|
27
|
+
top: 0,
|
|
28
|
+
width: "100%",
|
|
29
|
+
transition: "top 0s ease-in 0ms",
|
|
30
|
+
"&:hover": {
|
|
31
|
+
opacity: "0.3",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"&.cm-minimap-overlay-active": {
|
|
35
|
+
opacity: 1,
|
|
36
|
+
visibility: "visible",
|
|
37
|
+
transition: "visibility 0s linear 0ms, opacity 300ms",
|
|
38
|
+
"& .cm-minimap-overlay": {
|
|
39
|
+
opacity: "0.4",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const SCALE = Scale.PixelMultiplier * Scale.SizeRatio;
|
|
46
|
+
|
|
47
|
+
const OverlayView = ViewPlugin.fromClass(
|
|
48
|
+
class {
|
|
49
|
+
private container: HTMLElement | undefined;
|
|
50
|
+
private dom: HTMLElement | undefined;
|
|
51
|
+
|
|
52
|
+
private _isDragging: boolean = false;
|
|
53
|
+
private _dragStartY: number | undefined;
|
|
54
|
+
|
|
55
|
+
// Bound once so that removeEventListener sees the same reference that
|
|
56
|
+
// addEventListener was given; `.bind` at call time yields a new
|
|
57
|
+
// function each call and the listener would never be detached.
|
|
58
|
+
private readonly boundMouseDown = this.onMouseDown.bind(this);
|
|
59
|
+
private readonly boundMouseUp = this.onMouseUp.bind(this);
|
|
60
|
+
private readonly boundMouseMove = this.onMouseMove.bind(this);
|
|
61
|
+
|
|
62
|
+
public constructor(private view: EditorView) {
|
|
63
|
+
if (view.state.facet(Config).enabled) {
|
|
64
|
+
this.create(view);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private create(view: EditorView) {
|
|
69
|
+
this.container = crelt("div", {
|
|
70
|
+
class: "cm-minimap-overlay-container",
|
|
71
|
+
});
|
|
72
|
+
this.dom = crelt("div", { class: "cm-minimap-overlay" });
|
|
73
|
+
this.container.appendChild(this.dom);
|
|
74
|
+
|
|
75
|
+
// Attach event listeners for overlay
|
|
76
|
+
this.container.addEventListener("mousedown", this.boundMouseDown);
|
|
77
|
+
window.addEventListener("mouseup", this.boundMouseUp);
|
|
78
|
+
window.addEventListener("mousemove", this.boundMouseMove);
|
|
79
|
+
|
|
80
|
+
// Attach the overlay elements to the minimap
|
|
81
|
+
const inner = view.dom.querySelector(".cm-minimap-inner");
|
|
82
|
+
if (inner) {
|
|
83
|
+
inner.appendChild(this.container);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Initially set overlay configuration styles, height, top
|
|
87
|
+
this.computeShowOverlay();
|
|
88
|
+
this.computeHeight();
|
|
89
|
+
this.computeTop();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private remove() {
|
|
93
|
+
if (this.container) {
|
|
94
|
+
this.container.removeEventListener(
|
|
95
|
+
"mousedown",
|
|
96
|
+
this.boundMouseDown,
|
|
97
|
+
);
|
|
98
|
+
window.removeEventListener("mouseup", this.boundMouseUp);
|
|
99
|
+
window.removeEventListener("mousemove", this.boundMouseMove);
|
|
100
|
+
this.container.remove();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
update(update: ViewUpdate) {
|
|
105
|
+
const prev = update.startState.facet(Config).enabled;
|
|
106
|
+
const now = update.state.facet(Config).enabled;
|
|
107
|
+
|
|
108
|
+
if (prev && !now) {
|
|
109
|
+
this.remove();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!prev && now) {
|
|
114
|
+
this.create(update.view);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (now) {
|
|
118
|
+
this.computeShowOverlay();
|
|
119
|
+
|
|
120
|
+
if (update.geometryChanged) {
|
|
121
|
+
this.computeHeight();
|
|
122
|
+
this.computeTop();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public computeHeight() {
|
|
128
|
+
if (!this.dom) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const height = this.view.dom.clientHeight / SCALE;
|
|
133
|
+
this.dom.style.height = height + "px";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public computeTop() {
|
|
137
|
+
if (!this._isDragging && this.dom) {
|
|
138
|
+
const { clientHeight, scrollHeight, scrollTop } =
|
|
139
|
+
this.view.scrollDOM;
|
|
140
|
+
|
|
141
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
142
|
+
const topForNonOverflowing = scrollTop / SCALE;
|
|
143
|
+
|
|
144
|
+
const height = clientHeight / SCALE;
|
|
145
|
+
const maxTop = clientHeight - height;
|
|
146
|
+
let scrollRatio = scrollTop / maxScrollTop;
|
|
147
|
+
if (isNaN(scrollRatio)) scrollRatio = 0;
|
|
148
|
+
const topForOverflowing = maxTop * scrollRatio;
|
|
149
|
+
|
|
150
|
+
const top = Math.min(topForOverflowing, topForNonOverflowing);
|
|
151
|
+
this.dom.style.top = top + "px";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public computeShowOverlay() {
|
|
156
|
+
if (!this.container) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { showOverlay } = this.view.state.facet(Config);
|
|
161
|
+
|
|
162
|
+
if (showOverlay === "mouse-over") {
|
|
163
|
+
this.container.classList.add("cm-minimap-overlay-mouse-over");
|
|
164
|
+
} else {
|
|
165
|
+
this.container.classList.remove(
|
|
166
|
+
"cm-minimap-overlay-mouse-over",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { clientHeight, scrollHeight } = this.view.scrollDOM;
|
|
171
|
+
if (clientHeight === scrollHeight) {
|
|
172
|
+
this.container.classList.add("cm-minimap-overlay-off");
|
|
173
|
+
} else {
|
|
174
|
+
this.container.classList.remove("cm-minimap-overlay-off");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private onMouseDown(event: MouseEvent) {
|
|
179
|
+
if (!this.container) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Ignore right click
|
|
184
|
+
if (event.button === 2) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If target is the overlay start dragging
|
|
189
|
+
const { clientY, target } = event;
|
|
190
|
+
if (target === this.dom) {
|
|
191
|
+
this._dragStartY = event.clientY;
|
|
192
|
+
this._isDragging = true;
|
|
193
|
+
this.container.classList.add("cm-minimap-overlay-active");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Updates the scroll position of the EditorView based on the
|
|
198
|
+
// position of the MouseEvent on the minimap canvas
|
|
199
|
+
const { clientHeight, scrollHeight, scrollTop } =
|
|
200
|
+
this.view.scrollDOM;
|
|
201
|
+
const targetTop = (target as HTMLElement).getBoundingClientRect()
|
|
202
|
+
.top;
|
|
203
|
+
const deltaY = (clientY - targetTop) * SCALE;
|
|
204
|
+
|
|
205
|
+
const scrollRatio = scrollTop / (scrollHeight - clientHeight);
|
|
206
|
+
const visibleRange = clientHeight * SCALE - clientHeight;
|
|
207
|
+
const visibleTop = visibleRange * scrollRatio;
|
|
208
|
+
|
|
209
|
+
const top = Math.max(0, scrollTop - visibleTop);
|
|
210
|
+
this.view.scrollDOM.scrollTop = top + deltaY - clientHeight / 2;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private onMouseUp(_event: MouseEvent) {
|
|
214
|
+
// Stop dragging on mouseup
|
|
215
|
+
if (this._isDragging && this.container) {
|
|
216
|
+
this._dragStartY = undefined;
|
|
217
|
+
this._isDragging = false;
|
|
218
|
+
this.container.classList.remove("cm-minimap-overlay-active");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private onMouseMove(event: MouseEvent) {
|
|
223
|
+
if (!this._isDragging || !this.dom) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
event.stopPropagation();
|
|
229
|
+
|
|
230
|
+
// Without an existing position, we're just beginning to drag.
|
|
231
|
+
if (!this._dragStartY) {
|
|
232
|
+
this._dragStartY = event.clientY;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const deltaY = event.clientY - this._dragStartY;
|
|
237
|
+
const movingUp = deltaY < 0;
|
|
238
|
+
const movingDown = deltaY > 0;
|
|
239
|
+
|
|
240
|
+
// Update drag position for the next tick
|
|
241
|
+
this._dragStartY = event.clientY;
|
|
242
|
+
|
|
243
|
+
const canvasHeight = this.dom.getBoundingClientRect().height;
|
|
244
|
+
const canvasAbsTop = this.dom.getBoundingClientRect().y;
|
|
245
|
+
const canvasAbsBot = canvasAbsTop + canvasHeight;
|
|
246
|
+
const canvasRelTopDouble = parseFloat(this.dom.style.top);
|
|
247
|
+
|
|
248
|
+
const scrollPosition = this.view.scrollDOM.scrollTop;
|
|
249
|
+
const editorHeight = this.view.scrollDOM.clientHeight;
|
|
250
|
+
const contentHeight = this.view.scrollDOM.scrollHeight;
|
|
251
|
+
|
|
252
|
+
const atTop = scrollPosition === 0;
|
|
253
|
+
const atBottom =
|
|
254
|
+
Math.round(scrollPosition) >=
|
|
255
|
+
Math.round(contentHeight - editorHeight);
|
|
256
|
+
|
|
257
|
+
// We allow over-dragging past the top/bottom, but the overlay just sticks
|
|
258
|
+
// to the top or bottom of its range. These checks prevent us from immediately
|
|
259
|
+
// moving the overlay when the drag changes direction. We should wait until
|
|
260
|
+
// the cursor has returned to, and begun to pass the bottom/top of the range
|
|
261
|
+
if (
|
|
262
|
+
(atTop && movingUp) ||
|
|
263
|
+
(atTop && event.clientY < canvasAbsTop)
|
|
264
|
+
) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (
|
|
268
|
+
(atBottom && movingDown) ||
|
|
269
|
+
(atBottom && event.clientY > canvasAbsBot)
|
|
270
|
+
) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Set view scroll directly
|
|
275
|
+
const scrollHeight = this.view.scrollDOM.scrollHeight;
|
|
276
|
+
const clientHeight = this.view.scrollDOM.clientHeight;
|
|
277
|
+
|
|
278
|
+
const maxTopNonOverflowing = (scrollHeight - clientHeight) / SCALE;
|
|
279
|
+
const maxTopOverflowing = clientHeight - clientHeight / SCALE;
|
|
280
|
+
|
|
281
|
+
const change = canvasRelTopDouble + deltaY;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* ScrollPosOverflowing is calculated by:
|
|
285
|
+
* - Calculating the offset (change) relative to the total height of the container
|
|
286
|
+
* - Multiplying by the maximum scrollTop position for the scroller
|
|
287
|
+
* - The maximum scrollTop position for the scroller is the total scroll height minus the client height
|
|
288
|
+
*/
|
|
289
|
+
const relativeToMax = change / maxTopOverflowing;
|
|
290
|
+
const scrollPosOverflowing =
|
|
291
|
+
(scrollHeight - clientHeight) * relativeToMax;
|
|
292
|
+
|
|
293
|
+
const scrollPosNonOverflowing = change * SCALE;
|
|
294
|
+
this.view.scrollDOM.scrollTop = Math.max(
|
|
295
|
+
scrollPosOverflowing,
|
|
296
|
+
scrollPosNonOverflowing,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// view.scrollDOM truncates if out of bounds. We need to mimic that behavior here with min/max guard
|
|
300
|
+
const top = Math.min(
|
|
301
|
+
Math.max(0, change),
|
|
302
|
+
Math.min(maxTopOverflowing, maxTopNonOverflowing),
|
|
303
|
+
);
|
|
304
|
+
this.dom.style.top = top + "px";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public destroy() {
|
|
308
|
+
this.remove();
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
eventHandlers: {
|
|
313
|
+
scroll() {
|
|
314
|
+
requestAnimationFrame(() => this.computeTop());
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
export const Overlay = [Theme, OverlayView];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { EditorView, ViewUpdate } from "@codemirror/view";
|
|
2
|
+
import {
|
|
3
|
+
Diagnostic,
|
|
4
|
+
diagnosticCount,
|
|
5
|
+
forEachDiagnostic,
|
|
6
|
+
setDiagnosticsEffect,
|
|
7
|
+
} from "@codemirror/lint";
|
|
8
|
+
|
|
9
|
+
import { LineBasedState } from "./linebasedstate.js";
|
|
10
|
+
import { DrawContext } from "./types.js";
|
|
11
|
+
import { Lines, LinesState, foldsChanged } from "./LinesState.js";
|
|
12
|
+
import { Config } from "./Config.js";
|
|
13
|
+
|
|
14
|
+
type Severity = Diagnostic["severity"];
|
|
15
|
+
|
|
16
|
+
export class DiagnosticState extends LineBasedState<Severity> {
|
|
17
|
+
private count: number | undefined = undefined;
|
|
18
|
+
|
|
19
|
+
public constructor(view: EditorView) {
|
|
20
|
+
super(view);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private shouldUpdate(update: ViewUpdate) {
|
|
24
|
+
// If the minimap is disabled
|
|
25
|
+
if (!update.state.facet(Config).enabled) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If the doc changed
|
|
30
|
+
if (update.docChanged) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If the diagnostics changed
|
|
35
|
+
for (const tr of update.transactions) {
|
|
36
|
+
for (const ef of tr.effects) {
|
|
37
|
+
if (ef.is(setDiagnosticsEffect)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If the folds changed
|
|
44
|
+
if (foldsChanged(update.transactions)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// If the minimap was previously hidden
|
|
49
|
+
if (this.count === undefined) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public update(update: ViewUpdate) {
|
|
57
|
+
if (!this.shouldUpdate(update)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.map.clear();
|
|
62
|
+
const lines = update.state.field(LinesState);
|
|
63
|
+
this.count = diagnosticCount(update.state);
|
|
64
|
+
|
|
65
|
+
forEachDiagnostic(update.state, (diagnostic, from, to) => {
|
|
66
|
+
// Find the start and end lines for the diagnostic
|
|
67
|
+
const lineStart = this.findLine(from, lines);
|
|
68
|
+
const lineEnd = this.findLine(to, lines);
|
|
69
|
+
|
|
70
|
+
// Populate each line in the range with the highest severity diagnostic
|
|
71
|
+
let severity = diagnostic.severity;
|
|
72
|
+
for (let i = lineStart; i <= lineEnd; i++) {
|
|
73
|
+
const previous = this.get(i);
|
|
74
|
+
if (previous) {
|
|
75
|
+
severity = [severity, previous]
|
|
76
|
+
.sort(this.sort.bind(this))
|
|
77
|
+
.slice(0, 1)[0];
|
|
78
|
+
}
|
|
79
|
+
this.set(i, severity);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public drawLine(ctx: DrawContext, lineNumber: number) {
|
|
85
|
+
const { context, lineHeight, offsetX, offsetY } = ctx;
|
|
86
|
+
const severity = this.get(lineNumber);
|
|
87
|
+
if (!severity) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Draw the full line width rectangle in the background
|
|
92
|
+
context.globalAlpha = 0.65;
|
|
93
|
+
context.beginPath();
|
|
94
|
+
context.rect(
|
|
95
|
+
offsetX,
|
|
96
|
+
offsetY /* TODO Scaling causes anti-aliasing in rectangles */,
|
|
97
|
+
context.canvas.width - offsetX,
|
|
98
|
+
lineHeight,
|
|
99
|
+
);
|
|
100
|
+
context.fillStyle = this.color(severity);
|
|
101
|
+
context.fill();
|
|
102
|
+
|
|
103
|
+
// Restore full opacity, otherwise every layer drawn after this one --
|
|
104
|
+
// including the text of every subsequent line -- inherits the fade.
|
|
105
|
+
context.globalAlpha = 1;
|
|
106
|
+
|
|
107
|
+
// Draw diagnostic range rectangle in the foreground
|
|
108
|
+
// TODO: We need to update the state to have specific ranges
|
|
109
|
+
// context.globalAlpha = 1;
|
|
110
|
+
// context.beginPath();
|
|
111
|
+
// context.rect(offsetX, offsetY, textWidth, lineHeight);
|
|
112
|
+
// context.fillStyle = this.color(severity);
|
|
113
|
+
// context.fill();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Given a position and a set of line ranges, return
|
|
118
|
+
* the line number the position falls within
|
|
119
|
+
*/
|
|
120
|
+
private findLine(pos: number, lines: Lines) {
|
|
121
|
+
const index = lines.findIndex((spans) => {
|
|
122
|
+
const start = spans.slice(0, 1)[0];
|
|
123
|
+
const end = spans.slice(-1)[0];
|
|
124
|
+
|
|
125
|
+
if (!start || !end) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return start.from <= pos && pos <= end.to;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Line numbers begin at 1
|
|
133
|
+
return index + 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Colors from @codemirror/lint
|
|
138
|
+
* https://github.com/codemirror/lint/blob/e0671b43c02e72766ad1afe1579b7032fdcdb6c1/src/lint.ts#L597
|
|
139
|
+
*/
|
|
140
|
+
private color(severity: Severity) {
|
|
141
|
+
return severity === "error"
|
|
142
|
+
? "#d11"
|
|
143
|
+
: severity === "warning"
|
|
144
|
+
? "orange"
|
|
145
|
+
: "#999";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Sorts severity from most to least severe */
|
|
149
|
+
private sort(a: Severity, b: Severity) {
|
|
150
|
+
return this.score(b) - this.score(a);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Assigns a score to severity, with most severe being the highest */
|
|
154
|
+
private score(s: Severity) {
|
|
155
|
+
switch (s) {
|
|
156
|
+
case "error": {
|
|
157
|
+
return 3;
|
|
158
|
+
}
|
|
159
|
+
case "warning": {
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
default: {
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function diagnostics(view: EditorView): DiagnosticState {
|
|
170
|
+
return new DiagnosticState(view);
|
|
171
|
+
}
|