@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
import { Facet, combineConfig, StateField } from '@codemirror/state';
|
|
2
|
+
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
3
|
+
import crelt from 'crelt';
|
|
4
|
+
import { setDiagnosticsEffect, diagnosticCount, forEachDiagnostic } from '@codemirror/lint';
|
|
5
|
+
import { foldEffect, unfoldEffect, foldedRanges, language, highlightingFor } from '@codemirror/language';
|
|
6
|
+
import { highlightTree } from '@lezer/highlight';
|
|
7
|
+
import { TreeFragment } from '@lezer/common';
|
|
8
|
+
|
|
9
|
+
const Scale = {
|
|
10
|
+
// Multiply the number of canvas pixels
|
|
11
|
+
PixelMultiplier: 2,
|
|
12
|
+
// Downscale the editor contents by this ratio
|
|
13
|
+
SizeRatio: 4,
|
|
14
|
+
// Maximum width of the minimap in pixels
|
|
15
|
+
MaxWidth: 120
|
|
16
|
+
};
|
|
17
|
+
const Config = Facet.define({
|
|
18
|
+
combine: (c) => {
|
|
19
|
+
const configs = [];
|
|
20
|
+
for (const config of c) {
|
|
21
|
+
if (!config) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const { create, gutters, ...rest } = config;
|
|
25
|
+
configs.push({
|
|
26
|
+
...rest,
|
|
27
|
+
enabled: true,
|
|
28
|
+
gutters: gutters ? gutters.filter((v) => Object.keys(v).length > 0) : void 0
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return combineConfig(configs, {
|
|
32
|
+
enabled: configs.length > 0,
|
|
33
|
+
displayText: "characters",
|
|
34
|
+
eventHandlers: {},
|
|
35
|
+
showOverlay: "always",
|
|
36
|
+
gutters: [],
|
|
37
|
+
autohide: false,
|
|
38
|
+
width: Scale.MaxWidth,
|
|
39
|
+
hideScrollbar: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const Theme$1 = EditorView.theme({
|
|
45
|
+
".cm-minimap-overlay-container": {
|
|
46
|
+
position: "absolute",
|
|
47
|
+
top: 0,
|
|
48
|
+
height: "100%",
|
|
49
|
+
width: "100%",
|
|
50
|
+
"&.cm-minimap-overlay-mouse-over": {
|
|
51
|
+
opacity: 0,
|
|
52
|
+
transition: "visibility 0s linear 300ms, opacity 300ms"
|
|
53
|
+
},
|
|
54
|
+
"&.cm-minimap-overlay-mouse-over:hover": {
|
|
55
|
+
opacity: 1,
|
|
56
|
+
transition: "visibility 0s linear 0ms, opacity 300ms"
|
|
57
|
+
},
|
|
58
|
+
"&.cm-minimap-overlay-off": {
|
|
59
|
+
display: "none"
|
|
60
|
+
},
|
|
61
|
+
"& .cm-minimap-overlay": {
|
|
62
|
+
background: "rgb(121, 121, 121)",
|
|
63
|
+
opacity: "0.2",
|
|
64
|
+
position: "absolute",
|
|
65
|
+
right: 0,
|
|
66
|
+
top: 0,
|
|
67
|
+
width: "100%",
|
|
68
|
+
transition: "top 0s ease-in 0ms",
|
|
69
|
+
"&:hover": {
|
|
70
|
+
opacity: "0.3"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"&.cm-minimap-overlay-active": {
|
|
74
|
+
opacity: 1,
|
|
75
|
+
visibility: "visible",
|
|
76
|
+
transition: "visibility 0s linear 0ms, opacity 300ms",
|
|
77
|
+
"& .cm-minimap-overlay": {
|
|
78
|
+
opacity: "0.4"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const SCALE = Scale.PixelMultiplier * Scale.SizeRatio;
|
|
84
|
+
const OverlayView = ViewPlugin.fromClass(
|
|
85
|
+
class {
|
|
86
|
+
constructor(view) {
|
|
87
|
+
this.view = view;
|
|
88
|
+
if (view.state.facet(Config).enabled) {
|
|
89
|
+
this.create(view);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
view;
|
|
93
|
+
container;
|
|
94
|
+
dom;
|
|
95
|
+
_isDragging = false;
|
|
96
|
+
_dragStartY;
|
|
97
|
+
// Bound once so that removeEventListener sees the same reference that
|
|
98
|
+
// addEventListener was given; `.bind` at call time yields a new
|
|
99
|
+
// function each call and the listener would never be detached.
|
|
100
|
+
boundMouseDown = this.onMouseDown.bind(this);
|
|
101
|
+
boundMouseUp = this.onMouseUp.bind(this);
|
|
102
|
+
boundMouseMove = this.onMouseMove.bind(this);
|
|
103
|
+
create(view) {
|
|
104
|
+
this.container = crelt("div", {
|
|
105
|
+
class: "cm-minimap-overlay-container"
|
|
106
|
+
});
|
|
107
|
+
this.dom = crelt("div", { class: "cm-minimap-overlay" });
|
|
108
|
+
this.container.appendChild(this.dom);
|
|
109
|
+
this.container.addEventListener("mousedown", this.boundMouseDown);
|
|
110
|
+
window.addEventListener("mouseup", this.boundMouseUp);
|
|
111
|
+
window.addEventListener("mousemove", this.boundMouseMove);
|
|
112
|
+
const inner = view.dom.querySelector(".cm-minimap-inner");
|
|
113
|
+
if (inner) {
|
|
114
|
+
inner.appendChild(this.container);
|
|
115
|
+
}
|
|
116
|
+
this.computeShowOverlay();
|
|
117
|
+
this.computeHeight();
|
|
118
|
+
this.computeTop();
|
|
119
|
+
}
|
|
120
|
+
remove() {
|
|
121
|
+
if (this.container) {
|
|
122
|
+
this.container.removeEventListener(
|
|
123
|
+
"mousedown",
|
|
124
|
+
this.boundMouseDown
|
|
125
|
+
);
|
|
126
|
+
window.removeEventListener("mouseup", this.boundMouseUp);
|
|
127
|
+
window.removeEventListener("mousemove", this.boundMouseMove);
|
|
128
|
+
this.container.remove();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
update(update) {
|
|
132
|
+
const prev = update.startState.facet(Config).enabled;
|
|
133
|
+
const now = update.state.facet(Config).enabled;
|
|
134
|
+
if (prev && !now) {
|
|
135
|
+
this.remove();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!prev && now) {
|
|
139
|
+
this.create(update.view);
|
|
140
|
+
}
|
|
141
|
+
if (now) {
|
|
142
|
+
this.computeShowOverlay();
|
|
143
|
+
if (update.geometryChanged) {
|
|
144
|
+
this.computeHeight();
|
|
145
|
+
this.computeTop();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
computeHeight() {
|
|
150
|
+
if (!this.dom) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const height = this.view.dom.clientHeight / SCALE;
|
|
154
|
+
this.dom.style.height = height + "px";
|
|
155
|
+
}
|
|
156
|
+
computeTop() {
|
|
157
|
+
if (!this._isDragging && this.dom) {
|
|
158
|
+
const { clientHeight, scrollHeight, scrollTop } = this.view.scrollDOM;
|
|
159
|
+
const maxScrollTop = scrollHeight - clientHeight;
|
|
160
|
+
const topForNonOverflowing = scrollTop / SCALE;
|
|
161
|
+
const height = clientHeight / SCALE;
|
|
162
|
+
const maxTop = clientHeight - height;
|
|
163
|
+
let scrollRatio = scrollTop / maxScrollTop;
|
|
164
|
+
if (isNaN(scrollRatio)) scrollRatio = 0;
|
|
165
|
+
const topForOverflowing = maxTop * scrollRatio;
|
|
166
|
+
const top = Math.min(topForOverflowing, topForNonOverflowing);
|
|
167
|
+
this.dom.style.top = top + "px";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
computeShowOverlay() {
|
|
171
|
+
if (!this.container) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { showOverlay } = this.view.state.facet(Config);
|
|
175
|
+
if (showOverlay === "mouse-over") {
|
|
176
|
+
this.container.classList.add("cm-minimap-overlay-mouse-over");
|
|
177
|
+
} else {
|
|
178
|
+
this.container.classList.remove(
|
|
179
|
+
"cm-minimap-overlay-mouse-over"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const { clientHeight, scrollHeight } = this.view.scrollDOM;
|
|
183
|
+
if (clientHeight === scrollHeight) {
|
|
184
|
+
this.container.classList.add("cm-minimap-overlay-off");
|
|
185
|
+
} else {
|
|
186
|
+
this.container.classList.remove("cm-minimap-overlay-off");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
onMouseDown(event) {
|
|
190
|
+
if (!this.container) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (event.button === 2) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const { clientY, target } = event;
|
|
197
|
+
if (target === this.dom) {
|
|
198
|
+
this._dragStartY = event.clientY;
|
|
199
|
+
this._isDragging = true;
|
|
200
|
+
this.container.classList.add("cm-minimap-overlay-active");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { clientHeight, scrollHeight, scrollTop } = this.view.scrollDOM;
|
|
204
|
+
const targetTop = target.getBoundingClientRect().top;
|
|
205
|
+
const deltaY = (clientY - targetTop) * SCALE;
|
|
206
|
+
const scrollRatio = scrollTop / (scrollHeight - clientHeight);
|
|
207
|
+
const visibleRange = clientHeight * SCALE - clientHeight;
|
|
208
|
+
const visibleTop = visibleRange * scrollRatio;
|
|
209
|
+
const top = Math.max(0, scrollTop - visibleTop);
|
|
210
|
+
this.view.scrollDOM.scrollTop = top + deltaY - clientHeight / 2;
|
|
211
|
+
}
|
|
212
|
+
onMouseUp(_event) {
|
|
213
|
+
if (this._isDragging && this.container) {
|
|
214
|
+
this._dragStartY = void 0;
|
|
215
|
+
this._isDragging = false;
|
|
216
|
+
this.container.classList.remove("cm-minimap-overlay-active");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
onMouseMove(event) {
|
|
220
|
+
if (!this._isDragging || !this.dom) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
if (!this._dragStartY) {
|
|
226
|
+
this._dragStartY = event.clientY;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const deltaY = event.clientY - this._dragStartY;
|
|
230
|
+
const movingUp = deltaY < 0;
|
|
231
|
+
const movingDown = deltaY > 0;
|
|
232
|
+
this._dragStartY = event.clientY;
|
|
233
|
+
const canvasHeight = this.dom.getBoundingClientRect().height;
|
|
234
|
+
const canvasAbsTop = this.dom.getBoundingClientRect().y;
|
|
235
|
+
const canvasAbsBot = canvasAbsTop + canvasHeight;
|
|
236
|
+
const canvasRelTopDouble = parseFloat(this.dom.style.top);
|
|
237
|
+
const scrollPosition = this.view.scrollDOM.scrollTop;
|
|
238
|
+
const editorHeight = this.view.scrollDOM.clientHeight;
|
|
239
|
+
const contentHeight = this.view.scrollDOM.scrollHeight;
|
|
240
|
+
const atTop = scrollPosition === 0;
|
|
241
|
+
const atBottom = Math.round(scrollPosition) >= Math.round(contentHeight - editorHeight);
|
|
242
|
+
if (atTop && movingUp || atTop && event.clientY < canvasAbsTop) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (atBottom && movingDown || atBottom && event.clientY > canvasAbsBot) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const scrollHeight = this.view.scrollDOM.scrollHeight;
|
|
249
|
+
const clientHeight = this.view.scrollDOM.clientHeight;
|
|
250
|
+
const maxTopNonOverflowing = (scrollHeight - clientHeight) / SCALE;
|
|
251
|
+
const maxTopOverflowing = clientHeight - clientHeight / SCALE;
|
|
252
|
+
const change = canvasRelTopDouble + deltaY;
|
|
253
|
+
const relativeToMax = change / maxTopOverflowing;
|
|
254
|
+
const scrollPosOverflowing = (scrollHeight - clientHeight) * relativeToMax;
|
|
255
|
+
const scrollPosNonOverflowing = change * SCALE;
|
|
256
|
+
this.view.scrollDOM.scrollTop = Math.max(
|
|
257
|
+
scrollPosOverflowing,
|
|
258
|
+
scrollPosNonOverflowing
|
|
259
|
+
);
|
|
260
|
+
const top = Math.min(
|
|
261
|
+
Math.max(0, change),
|
|
262
|
+
Math.min(maxTopOverflowing, maxTopNonOverflowing)
|
|
263
|
+
);
|
|
264
|
+
this.dom.style.top = top + "px";
|
|
265
|
+
}
|
|
266
|
+
destroy() {
|
|
267
|
+
this.remove();
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
eventHandlers: {
|
|
272
|
+
scroll() {
|
|
273
|
+
requestAnimationFrame(() => this.computeTop());
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
const Overlay = [Theme$1, OverlayView];
|
|
279
|
+
|
|
280
|
+
class LineBasedState {
|
|
281
|
+
map;
|
|
282
|
+
view;
|
|
283
|
+
constructor(view) {
|
|
284
|
+
this.map = /* @__PURE__ */ new Map();
|
|
285
|
+
this.view = view;
|
|
286
|
+
}
|
|
287
|
+
get(lineNumber) {
|
|
288
|
+
return this.map.get(lineNumber);
|
|
289
|
+
}
|
|
290
|
+
set(lineNumber, value) {
|
|
291
|
+
this.map.set(lineNumber, value);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function computeLinesState(state) {
|
|
296
|
+
if (!state.facet(Config).enabled) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
const lines = [];
|
|
300
|
+
const lineCursor = state.doc.iterLines();
|
|
301
|
+
const foldedRangeCursor = foldedRanges(state).iter();
|
|
302
|
+
let textOffset = 0;
|
|
303
|
+
lineCursor.next();
|
|
304
|
+
while (!lineCursor.done) {
|
|
305
|
+
const lineText = lineCursor.value;
|
|
306
|
+
const from = textOffset;
|
|
307
|
+
const to = from + lineText.length;
|
|
308
|
+
while (foldedRangeCursor.value && foldedRangeCursor.to < from) {
|
|
309
|
+
foldedRangeCursor.next();
|
|
310
|
+
}
|
|
311
|
+
const { from: foldFrom, to: foldTo } = foldedRangeCursor;
|
|
312
|
+
const lineStartInFold = from >= foldFrom && from < foldTo;
|
|
313
|
+
const lineEndsInFold = to > foldFrom && to <= foldTo;
|
|
314
|
+
if (lineStartInFold) {
|
|
315
|
+
const lastLine = lines.pop() ?? [];
|
|
316
|
+
const lastRange = lastLine.pop();
|
|
317
|
+
if (lastRange && lastRange.folded) {
|
|
318
|
+
lastRange.to = foldTo;
|
|
319
|
+
}
|
|
320
|
+
if (lastRange) {
|
|
321
|
+
lastLine.push(lastRange);
|
|
322
|
+
}
|
|
323
|
+
if (!lastRange || !lastRange.folded) {
|
|
324
|
+
lastLine.push({ from: foldFrom, to: foldTo, folded: true });
|
|
325
|
+
}
|
|
326
|
+
if (!lineEndsInFold) {
|
|
327
|
+
lastLine.push({ from: foldTo, to, folded: false });
|
|
328
|
+
}
|
|
329
|
+
lines.push(lastLine);
|
|
330
|
+
} else if (lineEndsInFold) {
|
|
331
|
+
lines.push([
|
|
332
|
+
{ from, to: foldFrom, folded: false },
|
|
333
|
+
{ from: foldFrom, to: foldTo, folded: true }
|
|
334
|
+
]);
|
|
335
|
+
} else {
|
|
336
|
+
lines.push([{ from, to, folded: false }]);
|
|
337
|
+
}
|
|
338
|
+
textOffset = to + 1;
|
|
339
|
+
lineCursor.next();
|
|
340
|
+
}
|
|
341
|
+
return lines;
|
|
342
|
+
}
|
|
343
|
+
const LinesState = StateField.define({
|
|
344
|
+
create: (state) => computeLinesState(state),
|
|
345
|
+
update: (current, tr) => {
|
|
346
|
+
if (foldsChanged([tr]) || tr.docChanged) {
|
|
347
|
+
return computeLinesState(tr.state);
|
|
348
|
+
}
|
|
349
|
+
return current;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
function foldsChanged(transactions) {
|
|
353
|
+
return transactions.some(
|
|
354
|
+
(tr) => tr.effects.some((ef) => ef.is(foldEffect) || ef.is(unfoldEffect))
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
class DiagnosticState extends LineBasedState {
|
|
359
|
+
count = void 0;
|
|
360
|
+
constructor(view) {
|
|
361
|
+
super(view);
|
|
362
|
+
}
|
|
363
|
+
shouldUpdate(update) {
|
|
364
|
+
if (!update.state.facet(Config).enabled) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (update.docChanged) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
for (const tr of update.transactions) {
|
|
371
|
+
for (const ef of tr.effects) {
|
|
372
|
+
if (ef.is(setDiagnosticsEffect)) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (foldsChanged(update.transactions)) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (this.count === void 0) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
update(update) {
|
|
386
|
+
if (!this.shouldUpdate(update)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.map.clear();
|
|
390
|
+
const lines = update.state.field(LinesState);
|
|
391
|
+
this.count = diagnosticCount(update.state);
|
|
392
|
+
forEachDiagnostic(update.state, (diagnostic, from, to) => {
|
|
393
|
+
const lineStart = this.findLine(from, lines);
|
|
394
|
+
const lineEnd = this.findLine(to, lines);
|
|
395
|
+
let severity = diagnostic.severity;
|
|
396
|
+
for (let i = lineStart; i <= lineEnd; i++) {
|
|
397
|
+
const previous = this.get(i);
|
|
398
|
+
if (previous) {
|
|
399
|
+
severity = [severity, previous].sort(this.sort.bind(this)).slice(0, 1)[0];
|
|
400
|
+
}
|
|
401
|
+
this.set(i, severity);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
drawLine(ctx, lineNumber) {
|
|
406
|
+
const { context, lineHeight, offsetX, offsetY } = ctx;
|
|
407
|
+
const severity = this.get(lineNumber);
|
|
408
|
+
if (!severity) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
context.globalAlpha = 0.65;
|
|
412
|
+
context.beginPath();
|
|
413
|
+
context.rect(
|
|
414
|
+
offsetX,
|
|
415
|
+
offsetY,
|
|
416
|
+
context.canvas.width - offsetX,
|
|
417
|
+
lineHeight
|
|
418
|
+
);
|
|
419
|
+
context.fillStyle = this.color(severity);
|
|
420
|
+
context.fill();
|
|
421
|
+
context.globalAlpha = 1;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Given a position and a set of line ranges, return
|
|
425
|
+
* the line number the position falls within
|
|
426
|
+
*/
|
|
427
|
+
findLine(pos, lines) {
|
|
428
|
+
const index = lines.findIndex((spans) => {
|
|
429
|
+
const start = spans.slice(0, 1)[0];
|
|
430
|
+
const end = spans.slice(-1)[0];
|
|
431
|
+
if (!start || !end) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
return start.from <= pos && pos <= end.to;
|
|
435
|
+
});
|
|
436
|
+
return index + 1;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Colors from @codemirror/lint
|
|
440
|
+
* https://github.com/codemirror/lint/blob/e0671b43c02e72766ad1afe1579b7032fdcdb6c1/src/lint.ts#L597
|
|
441
|
+
*/
|
|
442
|
+
color(severity) {
|
|
443
|
+
return severity === "error" ? "#d11" : severity === "warning" ? "orange" : "#999";
|
|
444
|
+
}
|
|
445
|
+
/** Sorts severity from most to least severe */
|
|
446
|
+
sort(a, b) {
|
|
447
|
+
return this.score(b) - this.score(a);
|
|
448
|
+
}
|
|
449
|
+
/** Assigns a score to severity, with most severe being the highest */
|
|
450
|
+
score(s) {
|
|
451
|
+
switch (s) {
|
|
452
|
+
case "error": {
|
|
453
|
+
return 3;
|
|
454
|
+
}
|
|
455
|
+
case "warning": {
|
|
456
|
+
return 2;
|
|
457
|
+
}
|
|
458
|
+
default: {
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function diagnostics(view) {
|
|
465
|
+
return new DiagnosticState(view);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
class SelectionState extends LineBasedState {
|
|
469
|
+
_drawInfo;
|
|
470
|
+
_themeClasses;
|
|
471
|
+
constructor(view) {
|
|
472
|
+
super(view);
|
|
473
|
+
this.getDrawInfo();
|
|
474
|
+
this._themeClasses = view.dom.classList.value;
|
|
475
|
+
}
|
|
476
|
+
shouldUpdate(update) {
|
|
477
|
+
if (!update.state.facet(Config).enabled) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
if (update.docChanged) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (update.selectionSet) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (this._themeClasses !== this.view.dom.classList.value) {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
if (foldsChanged(update.transactions)) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
update(update) {
|
|
495
|
+
if (!this.shouldUpdate(update)) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.map.clear();
|
|
499
|
+
if (this._themeClasses !== this.view.dom.classList.value) {
|
|
500
|
+
this._drawInfo = void 0;
|
|
501
|
+
this._themeClasses = this.view.dom.classList.value;
|
|
502
|
+
}
|
|
503
|
+
const { ranges } = update.state.selection;
|
|
504
|
+
let selectionIndex = 0;
|
|
505
|
+
for (const [index, line] of update.state.field(LinesState).entries()) {
|
|
506
|
+
const selections2 = [];
|
|
507
|
+
let offset = 0;
|
|
508
|
+
for (const span of line) {
|
|
509
|
+
do {
|
|
510
|
+
if (selectionIndex >= ranges.length) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (span.to < ranges[selectionIndex].from) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (ranges[selectionIndex].from === ranges[selectionIndex].to) {
|
|
517
|
+
selectionIndex++;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const range = ranges[selectionIndex];
|
|
521
|
+
const selection = {
|
|
522
|
+
from: offset + Math.max(span.from, range.from) - span.from,
|
|
523
|
+
to: offset + Math.min(span.to, range.to) - span.from,
|
|
524
|
+
extends: range.to > span.to
|
|
525
|
+
};
|
|
526
|
+
const lastSelection = selections2.slice(-1)[0];
|
|
527
|
+
if (lastSelection && lastSelection.to === selection.from) {
|
|
528
|
+
let { to } = selection;
|
|
529
|
+
if (span.folded && selection.extends) {
|
|
530
|
+
to = selection.from + 1;
|
|
531
|
+
} else if (span.folded && !selection.extends) {
|
|
532
|
+
to = lastSelection.to;
|
|
533
|
+
}
|
|
534
|
+
selections2[selections2.length - 1] = {
|
|
535
|
+
...lastSelection,
|
|
536
|
+
to,
|
|
537
|
+
extends: selection.extends
|
|
538
|
+
};
|
|
539
|
+
} else if (!span.folded) {
|
|
540
|
+
selections2.push(selection);
|
|
541
|
+
}
|
|
542
|
+
if (selection.extends) {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
selectionIndex++;
|
|
546
|
+
} while (selectionIndex < ranges.length && span.to >= ranges[selectionIndex].from);
|
|
547
|
+
offset += span.folded ? 1 : span.to - span.from;
|
|
548
|
+
}
|
|
549
|
+
if (selections2.length === 0) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const lineNumber = index + 1;
|
|
553
|
+
this.map.set(lineNumber, selections2);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
drawLine(ctx, lineNumber) {
|
|
557
|
+
const {
|
|
558
|
+
context,
|
|
559
|
+
lineHeight,
|
|
560
|
+
charWidth,
|
|
561
|
+
offsetX: startOffsetX,
|
|
562
|
+
offsetY
|
|
563
|
+
} = ctx;
|
|
564
|
+
const selections2 = this.get(lineNumber);
|
|
565
|
+
if (!selections2) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
for (const selection of selections2) {
|
|
569
|
+
const offsetX = startOffsetX + selection.from * charWidth;
|
|
570
|
+
const textWidth = (selection.to - selection.from) * charWidth;
|
|
571
|
+
const fullWidth = context.canvas.width - offsetX;
|
|
572
|
+
if (selection.extends) {
|
|
573
|
+
context.globalAlpha = 0.65;
|
|
574
|
+
context.beginPath();
|
|
575
|
+
context.rect(offsetX, offsetY, fullWidth, lineHeight);
|
|
576
|
+
context.fillStyle = this.getDrawInfo().backgroundColor;
|
|
577
|
+
context.fill();
|
|
578
|
+
}
|
|
579
|
+
context.globalAlpha = 1;
|
|
580
|
+
context.beginPath();
|
|
581
|
+
context.rect(offsetX, offsetY, textWidth, lineHeight);
|
|
582
|
+
context.fillStyle = this.getDrawInfo().backgroundColor;
|
|
583
|
+
context.fill();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
getDrawInfo() {
|
|
587
|
+
if (this._drawInfo) {
|
|
588
|
+
return this._drawInfo;
|
|
589
|
+
}
|
|
590
|
+
const mockToken = document.createElement("span");
|
|
591
|
+
mockToken.setAttribute("class", "cm-selectionBackground");
|
|
592
|
+
this.view.dom.appendChild(mockToken);
|
|
593
|
+
const style = window.getComputedStyle(mockToken);
|
|
594
|
+
const result = { backgroundColor: style.backgroundColor };
|
|
595
|
+
this._drawInfo = result;
|
|
596
|
+
this.view.dom.removeChild(mockToken);
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function selections(view) {
|
|
601
|
+
return new SelectionState(view);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const NON_WHITESPACE = /\S+/g;
|
|
605
|
+
class TextState extends LineBasedState {
|
|
606
|
+
_previousTree;
|
|
607
|
+
_displayText;
|
|
608
|
+
_fontInfoMap = /* @__PURE__ */ new Map();
|
|
609
|
+
_themeClasses;
|
|
610
|
+
_highlightingCallbackId;
|
|
611
|
+
_charWidthCache;
|
|
612
|
+
/**
|
|
613
|
+
* Font metrics are derived from `getComputedStyle`, which is only correct
|
|
614
|
+
* once the fonts they describe have actually loaded. A web font that
|
|
615
|
+
* resolves after the first render would otherwise leave every cached
|
|
616
|
+
* measurement stale for the lifetime of the view.
|
|
617
|
+
*/
|
|
618
|
+
onFontsLoaded = () => {
|
|
619
|
+
this._fontInfoMap.clear();
|
|
620
|
+
this._charWidthCache = void 0;
|
|
621
|
+
};
|
|
622
|
+
constructor(view) {
|
|
623
|
+
super(view);
|
|
624
|
+
this._themeClasses = new Set(view.dom.classList.values());
|
|
625
|
+
document.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
|
|
626
|
+
if (view.state.facet(Config).enabled) {
|
|
627
|
+
this.updateImpl(view.state);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
destroy() {
|
|
631
|
+
document.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
|
|
632
|
+
}
|
|
633
|
+
shouldUpdate(update, themeChanged) {
|
|
634
|
+
if (update.docChanged) {
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
if (update.state.facet(Config) !== update.startState.facet(Config)) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
if (themeChanged) {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (foldsChanged(update.transactions)) {
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
update(update) {
|
|
649
|
+
const themeChanged = this.themeChanged();
|
|
650
|
+
if (!this.shouldUpdate(update, themeChanged)) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (this._highlightingCallbackId) {
|
|
654
|
+
if (typeof window.requestIdleCallback !== "undefined") {
|
|
655
|
+
cancelIdleCallback(this._highlightingCallbackId);
|
|
656
|
+
} else {
|
|
657
|
+
clearTimeout(this._highlightingCallbackId);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
this.updateImpl(update.state, update.changes, themeChanged);
|
|
661
|
+
}
|
|
662
|
+
updateImpl(state, changes, themeChanged = true) {
|
|
663
|
+
this.map.clear();
|
|
664
|
+
this._displayText = state.facet(Config).displayText;
|
|
665
|
+
if (themeChanged) {
|
|
666
|
+
this._fontInfoMap.clear();
|
|
667
|
+
this._charWidthCache = void 0;
|
|
668
|
+
}
|
|
669
|
+
let treeFragments = void 0;
|
|
670
|
+
if (this._previousTree && changes) {
|
|
671
|
+
const previousFragments = TreeFragment.addTree(this._previousTree);
|
|
672
|
+
const changedRanges = [];
|
|
673
|
+
changes.iterChangedRanges(
|
|
674
|
+
(fromA, toA, fromB, toB) => changedRanges.push({ fromA, toA, fromB, toB })
|
|
675
|
+
);
|
|
676
|
+
treeFragments = TreeFragment.applyChanges(
|
|
677
|
+
previousFragments,
|
|
678
|
+
changedRanges
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const docToString = state.doc.toString();
|
|
682
|
+
const parser = state.facet(language)?.parser;
|
|
683
|
+
const tree = parser ? parser.parse(docToString, treeFragments) : void 0;
|
|
684
|
+
this._previousTree = tree;
|
|
685
|
+
const highlighter = {
|
|
686
|
+
style: (tags) => highlightingFor(state, tags)
|
|
687
|
+
};
|
|
688
|
+
let highlights = [];
|
|
689
|
+
if (tree) {
|
|
690
|
+
const vpLineTop = state.doc.lineAt(this.view.viewport.from).number;
|
|
691
|
+
const vpLineBottom = state.doc.lineAt(this.view.viewport.to).number;
|
|
692
|
+
const vpLineCount = vpLineBottom - vpLineTop;
|
|
693
|
+
const vpScroll = vpLineTop / (state.doc.lines - vpLineCount);
|
|
694
|
+
const { SizeRatio, PixelMultiplier } = Scale;
|
|
695
|
+
const mmLineCount = vpLineCount * SizeRatio * PixelMultiplier;
|
|
696
|
+
const mmLineRatio = vpScroll * mmLineCount;
|
|
697
|
+
const mmLineTop = Math.max(1, Math.floor(vpLineTop - mmLineRatio));
|
|
698
|
+
const mmLineBottom = Math.min(
|
|
699
|
+
vpLineBottom + Math.floor(mmLineCount - mmLineRatio),
|
|
700
|
+
state.doc.lines
|
|
701
|
+
);
|
|
702
|
+
highlightTree(
|
|
703
|
+
tree,
|
|
704
|
+
highlighter,
|
|
705
|
+
(from, to, tags) => {
|
|
706
|
+
highlights.push({ from, to, tags });
|
|
707
|
+
},
|
|
708
|
+
state.doc.line(mmLineTop).from,
|
|
709
|
+
state.doc.line(mmLineBottom).to
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
this.updateMapImpl(state, highlights, docToString);
|
|
713
|
+
highlights = [];
|
|
714
|
+
const highlightingCallback = () => {
|
|
715
|
+
if (tree) {
|
|
716
|
+
highlightTree(tree, highlighter, (from, to, tags) => {
|
|
717
|
+
highlights.push({ from, to, tags });
|
|
718
|
+
});
|
|
719
|
+
this.updateMapImpl(state, highlights, docToString);
|
|
720
|
+
this._highlightingCallbackId = void 0;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
this._highlightingCallbackId = typeof window.requestIdleCallback !== "undefined" ? requestIdleCallback(highlightingCallback) : setTimeout(highlightingCallback);
|
|
724
|
+
}
|
|
725
|
+
updateMapImpl(state, highlights, docToString) {
|
|
726
|
+
this.map.clear();
|
|
727
|
+
const highlightsIterator = highlights.values();
|
|
728
|
+
let highlightPtr = highlightsIterator.next();
|
|
729
|
+
for (const [index, line] of state.field(LinesState).entries()) {
|
|
730
|
+
const spans = [];
|
|
731
|
+
for (const span of line) {
|
|
732
|
+
if (span.from === span.to) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (span.folded) {
|
|
736
|
+
spans.push({ text: "\u2026", tags: "" });
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
let position = span.from;
|
|
740
|
+
while (!highlightPtr.done && highlightPtr.value.from < span.to) {
|
|
741
|
+
const { from, to, tags } = highlightPtr.value;
|
|
742
|
+
if (to < position) {
|
|
743
|
+
highlightPtr = highlightsIterator.next();
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (from > position) {
|
|
747
|
+
spans.push({
|
|
748
|
+
text: docToString.slice(position, from),
|
|
749
|
+
tags: ""
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const start = Math.max(from, span.from);
|
|
753
|
+
const end = Math.min(to, span.to);
|
|
754
|
+
spans.push({ text: docToString.slice(start, end), tags });
|
|
755
|
+
position = end;
|
|
756
|
+
if (to > end) {
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
highlightPtr = highlightsIterator.next();
|
|
760
|
+
}
|
|
761
|
+
if (position !== span.to) {
|
|
762
|
+
spans.push({
|
|
763
|
+
text: docToString.slice(position, span.to),
|
|
764
|
+
tags: ""
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const lineNumber = index + 1;
|
|
769
|
+
this.map.set(lineNumber, spans);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
measure(context) {
|
|
773
|
+
const { color, font, lineHeight } = this.getFontInfo("");
|
|
774
|
+
context.textBaseline = "ideographic";
|
|
775
|
+
context.fillStyle = color;
|
|
776
|
+
context.font = font;
|
|
777
|
+
if (this._charWidthCache?.font !== font) {
|
|
778
|
+
this._charWidthCache = {
|
|
779
|
+
font,
|
|
780
|
+
charWidth: context.measureText("_").width
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return { charWidth: this._charWidthCache.charWidth, lineHeight };
|
|
784
|
+
}
|
|
785
|
+
drawLine(ctx, lineNumber) {
|
|
786
|
+
const line = this.get(lineNumber);
|
|
787
|
+
if (!line) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
let { context, charWidth, lineHeight, offsetX, offsetY } = ctx;
|
|
791
|
+
let prevInfo;
|
|
792
|
+
context.textBaseline = "ideographic";
|
|
793
|
+
for (const span of line) {
|
|
794
|
+
const info = this.getFontInfo(span.tags);
|
|
795
|
+
if (!prevInfo || prevInfo.color !== info.color) {
|
|
796
|
+
context.fillStyle = info.color;
|
|
797
|
+
}
|
|
798
|
+
if (!prevInfo || prevInfo.font !== info.font) {
|
|
799
|
+
context.font = info.font;
|
|
800
|
+
}
|
|
801
|
+
prevInfo = info;
|
|
802
|
+
lineHeight = Math.max(lineHeight, info.lineHeight);
|
|
803
|
+
switch (this._displayText) {
|
|
804
|
+
case "characters": {
|
|
805
|
+
context.fillText(span.text, offsetX, offsetY + lineHeight);
|
|
806
|
+
offsetX += span.text.length * charWidth;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case "blocks": {
|
|
810
|
+
NON_WHITESPACE.lastIndex = 0;
|
|
811
|
+
let start;
|
|
812
|
+
while ((start = NON_WHITESPACE.exec(span.text)) !== null) {
|
|
813
|
+
const startX = offsetX + start.index * charWidth;
|
|
814
|
+
let width = (NON_WHITESPACE.lastIndex - start.index) * charWidth;
|
|
815
|
+
if (startX > context.canvas.width) {
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
if (startX + width > context.canvas.width) {
|
|
819
|
+
width = context.canvas.width - startX;
|
|
820
|
+
}
|
|
821
|
+
const yBuffer = 2 / Scale.SizeRatio;
|
|
822
|
+
const height = lineHeight - yBuffer;
|
|
823
|
+
context.fillStyle = info.color;
|
|
824
|
+
context.globalAlpha = 0.65;
|
|
825
|
+
context.beginPath();
|
|
826
|
+
context.rect(startX, offsetY, width, height);
|
|
827
|
+
context.fill();
|
|
828
|
+
}
|
|
829
|
+
context.globalAlpha = 1;
|
|
830
|
+
offsetX += span.text.length * charWidth;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
getFontInfo(tags) {
|
|
837
|
+
const cached = this._fontInfoMap.get(tags);
|
|
838
|
+
if (cached) {
|
|
839
|
+
return cached;
|
|
840
|
+
}
|
|
841
|
+
const mockToken = crelt("span", { class: tags });
|
|
842
|
+
const mockLine = crelt(
|
|
843
|
+
"div",
|
|
844
|
+
{ class: "cm-line", style: "display: none" },
|
|
845
|
+
mockToken
|
|
846
|
+
);
|
|
847
|
+
this.view.contentDOM.appendChild(mockLine);
|
|
848
|
+
const style = window.getComputedStyle(mockToken);
|
|
849
|
+
const lineHeight = parseFloat(style.lineHeight) / Scale.SizeRatio;
|
|
850
|
+
const result = {
|
|
851
|
+
color: style.color,
|
|
852
|
+
font: `${style.fontStyle} ${style.fontWeight} ${lineHeight}px ${style.fontFamily}`,
|
|
853
|
+
lineHeight
|
|
854
|
+
};
|
|
855
|
+
this._fontInfoMap.set(tags, result);
|
|
856
|
+
this.view.contentDOM.removeChild(mockLine);
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
themeChanged() {
|
|
860
|
+
const previous = this._themeClasses;
|
|
861
|
+
const now = new Set(this.view.dom.classList.values());
|
|
862
|
+
this._themeClasses = now;
|
|
863
|
+
if (!previous) {
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
previous.delete("cm-focused");
|
|
867
|
+
now.delete("cm-focused");
|
|
868
|
+
if (previous.size !== now.size) {
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
let containsAll = true;
|
|
872
|
+
previous.forEach((theme) => {
|
|
873
|
+
if (!now.has(theme)) {
|
|
874
|
+
containsAll = false;
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
return !containsAll;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function text(view) {
|
|
881
|
+
return new TextState(view);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const GUTTER_WIDTH = 4;
|
|
885
|
+
function drawLineGutter(gutter, ctx, lineNumber) {
|
|
886
|
+
const color = gutter[lineNumber];
|
|
887
|
+
if (!color) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
ctx.context.fillStyle = color;
|
|
891
|
+
ctx.context.globalAlpha = 1;
|
|
892
|
+
ctx.context.beginPath();
|
|
893
|
+
ctx.context.rect(ctx.offsetX, ctx.offsetY, GUTTER_WIDTH, ctx.lineHeight);
|
|
894
|
+
ctx.context.fill();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const Theme = EditorView.theme({
|
|
898
|
+
"&": {
|
|
899
|
+
height: "100%",
|
|
900
|
+
overflowY: "auto"
|
|
901
|
+
},
|
|
902
|
+
"& .cm-minimap-gutter": {
|
|
903
|
+
borderRight: 0,
|
|
904
|
+
flexShrink: 0,
|
|
905
|
+
left: "unset",
|
|
906
|
+
position: "sticky",
|
|
907
|
+
right: 0,
|
|
908
|
+
top: 0
|
|
909
|
+
},
|
|
910
|
+
"& .cm-minimap-autohide": {
|
|
911
|
+
opacity: 0,
|
|
912
|
+
transition: "opacity 0.3s"
|
|
913
|
+
},
|
|
914
|
+
"& .cm-minimap-autohide:hover": {
|
|
915
|
+
opacity: 1
|
|
916
|
+
},
|
|
917
|
+
"& .cm-minimap-inner": {
|
|
918
|
+
height: "100%",
|
|
919
|
+
position: "absolute",
|
|
920
|
+
right: 0,
|
|
921
|
+
top: 0,
|
|
922
|
+
overflowY: "hidden",
|
|
923
|
+
"& canvas": {
|
|
924
|
+
display: "block"
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
"& .cm-minimap-box-shadow": {
|
|
928
|
+
boxShadow: "12px 0px 20px 5px #6c6c6c"
|
|
929
|
+
},
|
|
930
|
+
// Suppresses the scrollbar's appearance only; the scroller keeps its
|
|
931
|
+
// overflow, so wheel, trackpad, keyboard and programmatic scrolling all
|
|
932
|
+
// continue to work.
|
|
933
|
+
// Lives on the scroller rather than the editor root: `themeChanged`
|
|
934
|
+
// fingerprints the root's class list, so toggling a class there would read
|
|
935
|
+
// as a theme change and needlessly drop the font metric cache.
|
|
936
|
+
"& .cm-scroller.cm-minimap-hide-scrollbar": {
|
|
937
|
+
scrollbarWidth: "none",
|
|
938
|
+
"&::-webkit-scrollbar": {
|
|
939
|
+
display: "none"
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
const HIDE_SCROLLBAR_CLASS = "cm-minimap-hide-scrollbar";
|
|
944
|
+
const WIDTH_RATIO = 6;
|
|
945
|
+
const minimapClass = ViewPlugin.fromClass(
|
|
946
|
+
class {
|
|
947
|
+
constructor(view) {
|
|
948
|
+
this.view = view;
|
|
949
|
+
this.text = text(view);
|
|
950
|
+
this.selection = selections(view);
|
|
951
|
+
this.diagnostic = diagnostics(view);
|
|
952
|
+
if (view.state.facet(showMinimap)) {
|
|
953
|
+
this.create(view);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
view;
|
|
957
|
+
dom;
|
|
958
|
+
inner;
|
|
959
|
+
canvas;
|
|
960
|
+
text;
|
|
961
|
+
selection;
|
|
962
|
+
diagnostic;
|
|
963
|
+
create(view) {
|
|
964
|
+
const config = view.state.facet(showMinimap);
|
|
965
|
+
if (!config) {
|
|
966
|
+
throw Error("Expected nonnull");
|
|
967
|
+
}
|
|
968
|
+
this.inner = crelt("div", { class: "cm-minimap-inner" });
|
|
969
|
+
this.canvas = crelt("canvas");
|
|
970
|
+
this.dom = config.create(view).dom;
|
|
971
|
+
this.dom.classList.add("cm-gutters");
|
|
972
|
+
this.dom.classList.add("cm-minimap-gutter");
|
|
973
|
+
this.inner.appendChild(this.canvas);
|
|
974
|
+
this.dom.appendChild(this.inner);
|
|
975
|
+
this.view.scrollDOM.insertBefore(
|
|
976
|
+
this.dom,
|
|
977
|
+
this.view.contentDOM.nextSibling
|
|
978
|
+
);
|
|
979
|
+
for (const key in this.view.state.facet(Config).eventHandlers) {
|
|
980
|
+
const handler = this.view.state.facet(Config).eventHandlers[key];
|
|
981
|
+
if (handler) {
|
|
982
|
+
this.dom.addEventListener(
|
|
983
|
+
key,
|
|
984
|
+
(e) => handler(e, this.view)
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (config.autohide) {
|
|
989
|
+
this.dom.classList.add("cm-minimap-autohide");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
remove() {
|
|
993
|
+
this.view.scrollDOM.classList.remove(HIDE_SCROLLBAR_CLASS);
|
|
994
|
+
if (this.dom) {
|
|
995
|
+
this.dom.remove();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
update(update) {
|
|
999
|
+
const prev = update.startState.facet(showMinimap);
|
|
1000
|
+
const now = update.state.facet(showMinimap);
|
|
1001
|
+
if (prev && !now) {
|
|
1002
|
+
this.remove();
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (!prev && now) {
|
|
1006
|
+
this.create(update.view);
|
|
1007
|
+
}
|
|
1008
|
+
if (now) {
|
|
1009
|
+
this.text.update(update);
|
|
1010
|
+
this.selection.update(update);
|
|
1011
|
+
this.diagnostic.update(update);
|
|
1012
|
+
this.render();
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
getWidth() {
|
|
1016
|
+
const maxWidth = this.view.state.facet(Config).width;
|
|
1017
|
+
const editorWidth = this.view.dom.clientWidth;
|
|
1018
|
+
if (editorWidth <= maxWidth * WIDTH_RATIO) {
|
|
1019
|
+
return maxWidth * (editorWidth / (maxWidth * WIDTH_RATIO));
|
|
1020
|
+
}
|
|
1021
|
+
return maxWidth;
|
|
1022
|
+
}
|
|
1023
|
+
render() {
|
|
1024
|
+
if (!this.dom || !this.canvas || !this.inner) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.updateBoxShadow();
|
|
1028
|
+
this.view.scrollDOM.classList.toggle(
|
|
1029
|
+
HIDE_SCROLLBAR_CLASS,
|
|
1030
|
+
this.view.state.facet(Config).hideScrollbar
|
|
1031
|
+
);
|
|
1032
|
+
const width = this.getWidth();
|
|
1033
|
+
const domHeight = this.view.dom.getBoundingClientRect().height;
|
|
1034
|
+
this.dom.style.width = width + "px";
|
|
1035
|
+
this.canvas.style.maxWidth = width + "px";
|
|
1036
|
+
this.inner.style.minHeight = domHeight + "px";
|
|
1037
|
+
this.canvas.style.height = domHeight + "px";
|
|
1038
|
+
const canvasWidth = width * Scale.PixelMultiplier;
|
|
1039
|
+
const canvasHeight = domHeight * Scale.PixelMultiplier;
|
|
1040
|
+
const resized = this.canvas.width !== canvasWidth || this.canvas.height !== canvasHeight;
|
|
1041
|
+
if (resized) {
|
|
1042
|
+
this.canvas.width = canvasWidth;
|
|
1043
|
+
this.canvas.height = canvasHeight;
|
|
1044
|
+
}
|
|
1045
|
+
const context = this.canvas.getContext("2d");
|
|
1046
|
+
if (!context) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (!resized) {
|
|
1050
|
+
context.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
1051
|
+
}
|
|
1052
|
+
context.globalAlpha = 1;
|
|
1053
|
+
const { charWidth, lineHeight } = this.text.measure(context);
|
|
1054
|
+
let { startIndex, endIndex, offsetY } = this.canvasStartAndEndIndex(
|
|
1055
|
+
context,
|
|
1056
|
+
lineHeight
|
|
1057
|
+
);
|
|
1058
|
+
const gutters = this.view.state.facet(Config).gutters;
|
|
1059
|
+
const lineCount = this.view.state.field(LinesState).length;
|
|
1060
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
1061
|
+
if (i >= lineCount) break;
|
|
1062
|
+
const drawContext = {
|
|
1063
|
+
offsetX: 0,
|
|
1064
|
+
offsetY,
|
|
1065
|
+
context,
|
|
1066
|
+
lineHeight,
|
|
1067
|
+
charWidth
|
|
1068
|
+
};
|
|
1069
|
+
if (gutters.length) {
|
|
1070
|
+
drawContext.offsetX += 2;
|
|
1071
|
+
for (const gutter of gutters) {
|
|
1072
|
+
drawLineGutter(gutter, drawContext, i + 1);
|
|
1073
|
+
drawContext.offsetX += GUTTER_WIDTH;
|
|
1074
|
+
}
|
|
1075
|
+
drawContext.offsetX += 2;
|
|
1076
|
+
}
|
|
1077
|
+
this.text.drawLine(drawContext, i + 1);
|
|
1078
|
+
this.selection.drawLine(drawContext, i + 1);
|
|
1079
|
+
this.diagnostic.drawLine(drawContext, i + 1);
|
|
1080
|
+
offsetY += lineHeight;
|
|
1081
|
+
}
|
|
1082
|
+
context.restore();
|
|
1083
|
+
}
|
|
1084
|
+
canvasStartAndEndIndex(context, lineHeight) {
|
|
1085
|
+
const { top: rawTop, bottom: rawBottom } = this.view.documentPadding;
|
|
1086
|
+
const pTop = rawTop / Scale.SizeRatio;
|
|
1087
|
+
const pBottom = rawBottom / Scale.SizeRatio;
|
|
1088
|
+
const canvasHeight = context.canvas.height;
|
|
1089
|
+
const { clientHeight, scrollHeight, scrollTop } = this.view.scrollDOM;
|
|
1090
|
+
let scrollPercent = scrollTop / (scrollHeight - clientHeight);
|
|
1091
|
+
if (isNaN(scrollPercent)) {
|
|
1092
|
+
scrollPercent = 0;
|
|
1093
|
+
}
|
|
1094
|
+
const lineCount = this.view.state.field(LinesState).length;
|
|
1095
|
+
const totalHeight = pTop + pBottom + lineCount * lineHeight;
|
|
1096
|
+
const canvasTop = Math.max(
|
|
1097
|
+
0,
|
|
1098
|
+
scrollPercent * (totalHeight - canvasHeight)
|
|
1099
|
+
);
|
|
1100
|
+
const offsetY = Math.max(0, pTop - canvasTop);
|
|
1101
|
+
const startIndex = Math.round(
|
|
1102
|
+
Math.max(0, canvasTop - pTop) / lineHeight
|
|
1103
|
+
);
|
|
1104
|
+
const spaceForLines = Math.round(
|
|
1105
|
+
(canvasHeight - offsetY) / lineHeight
|
|
1106
|
+
);
|
|
1107
|
+
return {
|
|
1108
|
+
startIndex,
|
|
1109
|
+
endIndex: startIndex + spaceForLines,
|
|
1110
|
+
offsetY
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
updateBoxShadow() {
|
|
1114
|
+
if (!this.canvas) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const { clientWidth, scrollWidth, scrollLeft } = this.view.scrollDOM;
|
|
1118
|
+
if (clientWidth + scrollLeft < scrollWidth) {
|
|
1119
|
+
this.canvas.classList.add("cm-minimap-box-shadow");
|
|
1120
|
+
} else {
|
|
1121
|
+
this.canvas.classList.remove("cm-minimap-box-shadow");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
destroy() {
|
|
1125
|
+
this.text.destroy();
|
|
1126
|
+
this.remove();
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
eventHandlers: {
|
|
1131
|
+
scroll() {
|
|
1132
|
+
requestAnimationFrame(() => this.render());
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
provide: (plugin) => {
|
|
1136
|
+
return EditorView.scrollMargins.of((view) => {
|
|
1137
|
+
const width = view.plugin(plugin)?.getWidth();
|
|
1138
|
+
if (!width) {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
return { right: width };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
);
|
|
1146
|
+
const showMinimap = Facet.define({
|
|
1147
|
+
combine: (c) => c.find((o) => o !== null) ?? null,
|
|
1148
|
+
enables: (f) => {
|
|
1149
|
+
return [
|
|
1150
|
+
[
|
|
1151
|
+
Config.compute([f], (s) => s.facet(f)),
|
|
1152
|
+
Theme,
|
|
1153
|
+
LinesState,
|
|
1154
|
+
minimapClass,
|
|
1155
|
+
// TODO, codemirror-ify this one better
|
|
1156
|
+
Overlay
|
|
1157
|
+
]
|
|
1158
|
+
];
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
export { showMinimap };
|
|
1163
|
+
//# sourceMappingURL=index.js.map
|