@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/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