@batchfy/codemirror-minimap 0.1.0 → 0.1.1
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 +5 -0
- package/dist/Overlay.d.ts.map +1 -1
- package/dist/geometry.d.ts +130 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +321 -166
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/Overlay.ts +151 -118
- package/src/geometry.ts +402 -0
- package/src/index.ts +39 -25
package/src/geometry.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { BlockInfo, EditorView } from "@codemirror/view";
|
|
2
|
+
import { Scale } from "./Config.js";
|
|
3
|
+
import { LinesState, type Lines } from "./LinesState.js";
|
|
4
|
+
|
|
5
|
+
/** Editor pixels that fit into a single minimap pixel. */
|
|
6
|
+
export const SCALE = Scale.PixelMultiplier * Scale.SizeRatio;
|
|
7
|
+
|
|
8
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
9
|
+
Math.min(Math.max(value, min), max);
|
|
10
|
+
|
|
11
|
+
export type LayoutInput = {
|
|
12
|
+
/** Rows the minimap draws: one per line it lays out, folds collapsed. */
|
|
13
|
+
rowCount: number;
|
|
14
|
+
/** Height of one row. */
|
|
15
|
+
lineHeight: number;
|
|
16
|
+
paddingTop: number;
|
|
17
|
+
paddingBottom: number;
|
|
18
|
+
/** Height of the visible minimap. */
|
|
19
|
+
height: number;
|
|
20
|
+
/** Rows the editor currently shows on screen, inclusive. */
|
|
21
|
+
firstVisibleRow: number;
|
|
22
|
+
lastVisibleRow: number;
|
|
23
|
+
/**
|
|
24
|
+
* Rows a screenful holds when none of its lines wrap.
|
|
25
|
+
*
|
|
26
|
+
* How many rows are on screen right now swings with how much the lines on
|
|
27
|
+
* it wrap: a screen of code holds many lines, a screen of prose few. This
|
|
28
|
+
* count follows from the editor's height and its line height alone, so it
|
|
29
|
+
* holds still while the document scrolls.
|
|
30
|
+
*/
|
|
31
|
+
rowsPerScreen: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Layout = {
|
|
35
|
+
/** Height of every row plus the document padding. */
|
|
36
|
+
contentHeight: number;
|
|
37
|
+
/** How far the rows are scrolled up inside the visible minimap. */
|
|
38
|
+
scrollTop: number;
|
|
39
|
+
/** Position of the viewport overlay inside the visible minimap. */
|
|
40
|
+
overlayTop: number;
|
|
41
|
+
overlayHeight: number;
|
|
42
|
+
/** Rows the minimap has to draw, carried through for the inverse. */
|
|
43
|
+
rowCount: number;
|
|
44
|
+
/**
|
|
45
|
+
* Rows the document can scroll through if none of its lines wrap: the ones
|
|
46
|
+
* past the first screenful.
|
|
47
|
+
*
|
|
48
|
+
* Wrapping puts fewer document lines on a screen than there are rows in it,
|
|
49
|
+
* so the editor can carry on past this — `rowAtOverlayTop` covers the rest.
|
|
50
|
+
*/
|
|
51
|
+
scrollableRows: number;
|
|
52
|
+
/**
|
|
53
|
+
* How far the overlay's top edge moves between the first row of the
|
|
54
|
+
* document and the last, and so how far a drag has to travel to cross the
|
|
55
|
+
* document. It is `overlayTravel / scrollableRows` per row, which is what
|
|
56
|
+
* `rowAtOverlayTop` inverts.
|
|
57
|
+
*/
|
|
58
|
+
overlayTravel: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Places the rows and the viewport overlay together.
|
|
63
|
+
*
|
|
64
|
+
* The overlay marks lines, not pixels, so its position cannot be derived from
|
|
65
|
+
* the editor's scroll offset: a wrapped line takes several rows of the editor
|
|
66
|
+
* and one row of the minimap, so equal scroll fractions stop meaning equal
|
|
67
|
+
* positions as soon as wrapping, folding or line-height variation enters the
|
|
68
|
+
* document. Both halves are therefore anchored on the rows that are actually on
|
|
69
|
+
* screen, which keeps the overlay on top of the content it stands for.
|
|
70
|
+
*
|
|
71
|
+
* The rows scroll in step with progress through the document, so the overlay
|
|
72
|
+
* ends up where that progress says it should: at the top on the first row, at
|
|
73
|
+
* the bottom on the last. Near either end, and whenever the rows have less to
|
|
74
|
+
* give than the progress asks for, the rows stop and the overlay takes over the
|
|
75
|
+
* remaining travel — covering its own rows always wins over sitting where the
|
|
76
|
+
* scroll fraction would like it to.
|
|
77
|
+
*/
|
|
78
|
+
export function computeLayout(input: LayoutInput): Layout {
|
|
79
|
+
const { rowCount, lineHeight, paddingTop, paddingBottom, height } = input;
|
|
80
|
+
|
|
81
|
+
const lastRow = Math.max(0, rowCount - 1);
|
|
82
|
+
const first = clamp(input.firstVisibleRow, 0, lastRow);
|
|
83
|
+
const last = clamp(input.lastVisibleRow, first, lastRow);
|
|
84
|
+
const visibleRows = last - first + 1;
|
|
85
|
+
|
|
86
|
+
const contentHeight = paddingTop + paddingBottom + rowCount * lineHeight;
|
|
87
|
+
const overlayHeight = Math.min(height, visibleRows * lineHeight);
|
|
88
|
+
const rowTop = paddingTop + first * lineHeight;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Measured against a screenful of unwrapped lines rather than against the
|
|
92
|
+
* rows that happen to be on screen. A denominator that moved with the
|
|
93
|
+
* current wrapping would shrink as a screen of code scrolled into a screen
|
|
94
|
+
* of wrapped prose, and the rows would slide backwards underneath an
|
|
95
|
+
* overlay that had just moved forwards.
|
|
96
|
+
*/
|
|
97
|
+
const scrollableRows = Math.max(0, rowCount - input.rowsPerScreen);
|
|
98
|
+
const progress =
|
|
99
|
+
scrollableRows <= 0 ? 0 : clamp(first / scrollableRows, 0, 1);
|
|
100
|
+
|
|
101
|
+
const maxScroll = Math.max(0, contentHeight - height);
|
|
102
|
+
const scrollTop = clamp(
|
|
103
|
+
maxScroll * progress,
|
|
104
|
+
Math.max(0, rowTop - (height - overlayHeight)),
|
|
105
|
+
Math.min(maxScroll, rowTop),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Measured against a screenful of rows rather than against the overlay's
|
|
110
|
+
* current height, for the same reason `progress` is: an overlay shrunk by
|
|
111
|
+
* the wrapping on screen right now would report a travel that the rows it
|
|
112
|
+
* has to move over cannot back up, and a drag would run ahead of them.
|
|
113
|
+
*/
|
|
114
|
+
const overlayTravel = Math.max(
|
|
115
|
+
0,
|
|
116
|
+
Math.min(height, contentHeight) -
|
|
117
|
+
paddingTop -
|
|
118
|
+
paddingBottom -
|
|
119
|
+
input.rowsPerScreen * lineHeight,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
contentHeight,
|
|
124
|
+
scrollTop,
|
|
125
|
+
overlayTop: rowTop - scrollTop,
|
|
126
|
+
overlayHeight,
|
|
127
|
+
rowCount,
|
|
128
|
+
scrollableRows,
|
|
129
|
+
overlayTravel,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type Geometry = Layout & {
|
|
134
|
+
lineHeight: number;
|
|
135
|
+
paddingTop: number;
|
|
136
|
+
paddingBottom: number;
|
|
137
|
+
/** Height of the visible minimap. */
|
|
138
|
+
height: number;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The part of the geometry that scrolling does not change.
|
|
143
|
+
*
|
|
144
|
+
* Everything here follows from the document's length, the theme's row height
|
|
145
|
+
* and the size of the editor, so it holds still while the document moves under
|
|
146
|
+
* the overlay. A drag can therefore measure it once at the outset instead of on
|
|
147
|
+
* every report from the pointer — and the type is the argument that this is
|
|
148
|
+
* safe, since a function given only these values cannot read the scroll
|
|
149
|
+
* position it would be caching past.
|
|
150
|
+
*/
|
|
151
|
+
export type StableGeometry = Pick<
|
|
152
|
+
Geometry,
|
|
153
|
+
| "paddingTop"
|
|
154
|
+
| "lineHeight"
|
|
155
|
+
| "rowCount"
|
|
156
|
+
| "scrollableRows"
|
|
157
|
+
| "overlayTravel"
|
|
158
|
+
>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The row height the renderer measured, in minimap pixels.
|
|
162
|
+
*
|
|
163
|
+
* It comes from the computed style of a mock line, which only the renderer is
|
|
164
|
+
* set up to measure, and it changes with the theme rather than with the scroll
|
|
165
|
+
* position. Sharing the measurement lets the overlay lay itself out against the
|
|
166
|
+
* exact grid the rows were drawn on instead of a second estimate of it.
|
|
167
|
+
*/
|
|
168
|
+
const rowHeights = new WeakMap<EditorView, number>();
|
|
169
|
+
|
|
170
|
+
export function setRowHeight(view: EditorView, canvasLineHeight: number) {
|
|
171
|
+
const lineHeight = canvasLineHeight / Scale.PixelMultiplier;
|
|
172
|
+
|
|
173
|
+
if (rowHeights.get(view) !== lineHeight) {
|
|
174
|
+
rowHeights.set(view, lineHeight);
|
|
175
|
+
measured.delete(view);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Index of the row the minimap draws the given document position on. */
|
|
180
|
+
export function rowAtPos(lines: Lines, pos: number): number {
|
|
181
|
+
let low = 0;
|
|
182
|
+
let high = lines.length - 1;
|
|
183
|
+
|
|
184
|
+
while (low < high) {
|
|
185
|
+
const mid = Math.ceil((low + high) / 2);
|
|
186
|
+
const line = lines[mid];
|
|
187
|
+
|
|
188
|
+
if (line && line.length && line[0].from <= pos) {
|
|
189
|
+
low = mid;
|
|
190
|
+
} else {
|
|
191
|
+
high = mid - 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Math.max(0, low);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The line block at a vertical position, found by bisecting the document.
|
|
200
|
+
*
|
|
201
|
+
* `view.lineBlockAtHeight` answers this directly, but it refuses to run while
|
|
202
|
+
* the editor is mid-update, which is when the minimap draws. Blocks are ordered
|
|
203
|
+
* by height, so the same answer is reachable through `view.lineBlockAt`, which
|
|
204
|
+
* carries no such restriction.
|
|
205
|
+
*/
|
|
206
|
+
function lineBlockAtHeight(view: EditorView, height: number): BlockInfo {
|
|
207
|
+
const { doc } = view.state;
|
|
208
|
+
let low = 1;
|
|
209
|
+
let high = doc.lines;
|
|
210
|
+
|
|
211
|
+
while (low < high) {
|
|
212
|
+
const mid = Math.floor((low + high) / 2);
|
|
213
|
+
|
|
214
|
+
if (view.lineBlockAt(doc.line(mid).from).bottom <= height) {
|
|
215
|
+
low = mid + 1;
|
|
216
|
+
} else {
|
|
217
|
+
high = mid;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return view.lineBlockAt(doc.line(low).from);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The rows the editor currently shows on screen, inclusive. */
|
|
225
|
+
function visibleRows(view: EditorView): { first: number; last: number } {
|
|
226
|
+
const lines = view.state.field(LinesState);
|
|
227
|
+
const { top, bottom } = view.scrollDOM.getBoundingClientRect();
|
|
228
|
+
const documentTop = view.documentTop;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
first: rowAtPos(lines, lineBlockAtHeight(view, top - documentTop).from),
|
|
232
|
+
last: rowAtPos(lines, lineBlockAtHeight(view, bottom - documentTop).to),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The last geometry measured for a view, and what it was measured for.
|
|
238
|
+
*
|
|
239
|
+
* The overlay and the renderer both need the geometry and both run on every
|
|
240
|
+
* update, so measuring it twice means paying twice for the three
|
|
241
|
+
* `getBoundingClientRect` calls behind `view.dom`, `view.scrollDOM` and
|
|
242
|
+
* `view.documentTop`, and for the two bisections of the height map. Handing
|
|
243
|
+
* both of them the same token — the `ViewUpdate` or the scroll event they are
|
|
244
|
+
* responding to — lets the second one reuse the first one's answer.
|
|
245
|
+
*/
|
|
246
|
+
const measured = new WeakMap<
|
|
247
|
+
EditorView,
|
|
248
|
+
{ token: object; geometry: Geometry }
|
|
249
|
+
>();
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Where the minimap draws its rows and its overlay for the current state.
|
|
253
|
+
*
|
|
254
|
+
* `token` stands for the moment being drawn: anything measured for the same
|
|
255
|
+
* token describes the same moment, so it is measured once. Callers with no
|
|
256
|
+
* moment to name pass nothing and get a fresh measurement.
|
|
257
|
+
*/
|
|
258
|
+
export function readGeometry(view: EditorView, token?: object): Geometry {
|
|
259
|
+
if (token) {
|
|
260
|
+
const previous = measured.get(view);
|
|
261
|
+
|
|
262
|
+
if (previous && previous.token === token) {
|
|
263
|
+
return previous.geometry;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const geometry = measureGeometry(view);
|
|
268
|
+
|
|
269
|
+
if (token) {
|
|
270
|
+
measured.set(view, { token, geometry });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return geometry;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function measureGeometry(view: EditorView): Geometry {
|
|
277
|
+
const lineHeight =
|
|
278
|
+
rowHeights.get(view) || view.defaultLineHeight / SCALE || 0;
|
|
279
|
+
const paddingTop = view.documentPadding.top / SCALE;
|
|
280
|
+
const paddingBottom = view.documentPadding.bottom / SCALE;
|
|
281
|
+
const height = view.dom.getBoundingClientRect().height;
|
|
282
|
+
const { first, last } = visibleRows(view);
|
|
283
|
+
|
|
284
|
+
const layout = computeLayout({
|
|
285
|
+
rowCount: view.state.field(LinesState).length,
|
|
286
|
+
lineHeight,
|
|
287
|
+
paddingTop,
|
|
288
|
+
paddingBottom,
|
|
289
|
+
height,
|
|
290
|
+
firstVisibleRow: first,
|
|
291
|
+
lastVisibleRow: last,
|
|
292
|
+
rowsPerScreen: view.defaultLineHeight
|
|
293
|
+
? view.scrollDOM.clientHeight / view.defaultLineHeight
|
|
294
|
+
: 0,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return { ...layout, lineHeight, paddingTop, paddingBottom, height };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The row drawn `y` pixels below the top of the visible minimap. */
|
|
301
|
+
export function rowAtY(geometry: Geometry, y: number): number {
|
|
302
|
+
if (geometry.lineHeight <= 0) {
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const offset = geometry.scrollTop + y - geometry.paddingTop;
|
|
307
|
+
return Math.floor(offset / geometry.lineHeight);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The row the editor would have to put at the top of its viewport for the
|
|
312
|
+
* overlay to sit at `overlayTop`.
|
|
313
|
+
*
|
|
314
|
+
* This is the inverse of the placement `computeLayout` performs, so dragging
|
|
315
|
+
* the overlay to a position and letting the layout put it back lands on the
|
|
316
|
+
* same pixel. It cannot be read off `rowAtY`: the rows underneath the overlay
|
|
317
|
+
* scroll as the overlay moves, so the row drawn at a position is not the row
|
|
318
|
+
* that would be on screen were the overlay taken there.
|
|
319
|
+
*
|
|
320
|
+
* The answer carries its fraction. A whole number of rows is not enough to
|
|
321
|
+
* reach the end of a document: the editor stops with the last line against the
|
|
322
|
+
* foot of the viewport, which in general falls part-way through a row, and a
|
|
323
|
+
* drag that could only name whole rows would stop short of it by that part and
|
|
324
|
+
* need a jump to finish.
|
|
325
|
+
*/
|
|
326
|
+
export function rowAtOverlayTop(
|
|
327
|
+
geometry: StableGeometry,
|
|
328
|
+
overlayTop: number,
|
|
329
|
+
): number {
|
|
330
|
+
if (
|
|
331
|
+
geometry.overlayTravel <= 0 ||
|
|
332
|
+
geometry.scrollableRows <= 0 ||
|
|
333
|
+
geometry.lineHeight <= 0
|
|
334
|
+
) {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const offset = overlayTop - geometry.paddingTop;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Past its travel the overlay is still moving, and the drag has to follow
|
|
342
|
+
* it there.
|
|
343
|
+
*
|
|
344
|
+
* `scrollableRows` counts rows against a screenful of unwrapped lines, but
|
|
345
|
+
* a screen of wrapped lines holds fewer document lines than it has rows,
|
|
346
|
+
* so the editor can put rows past that one at the top of its viewport. The
|
|
347
|
+
* layout already draws them: `progress` stops at 1, the rows stop
|
|
348
|
+
* scrolling, and the overlay covers the remaining distance on its own, one
|
|
349
|
+
* row height per row. Ending the inverse at `scrollableRows` instead left a
|
|
350
|
+
* drag stuck a screenful's worth of wrapping short of the end, reachable
|
|
351
|
+
* only by scrolling the editor itself.
|
|
352
|
+
*/
|
|
353
|
+
if (offset >= geometry.overlayTravel) {
|
|
354
|
+
return clamp(
|
|
355
|
+
geometry.scrollableRows +
|
|
356
|
+
(offset - geometry.overlayTravel) / geometry.lineHeight,
|
|
357
|
+
0,
|
|
358
|
+
Math.max(0, geometry.rowCount - 1),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const progress = clamp(offset / geometry.overlayTravel, 0, 1);
|
|
363
|
+
|
|
364
|
+
return progress * geometry.scrollableRows;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Scrolls the editor until `row` sits `offset` pixels down the viewport.
|
|
369
|
+
*
|
|
370
|
+
* `row` may name a point inside a row rather than the start of one, and the
|
|
371
|
+
* fraction is taken against that row's own height so that it means the same
|
|
372
|
+
* part of a wrapped line as of an unwrapped one. Scrolling to whole rows is
|
|
373
|
+
* enough for a click, which is aiming at a line; a drag needs the fraction, or
|
|
374
|
+
* it cannot follow the pointer through the last part of a row and stops short
|
|
375
|
+
* of the end of the document.
|
|
376
|
+
*/
|
|
377
|
+
export function scrollToRow(view: EditorView, row: number, offset = 0) {
|
|
378
|
+
const lines = view.state.field(LinesState);
|
|
379
|
+
const wanted = clamp(row, 0, lines.length - 1);
|
|
380
|
+
const index = Math.floor(wanted);
|
|
381
|
+
const line = lines[index];
|
|
382
|
+
|
|
383
|
+
if (!line || !line.length) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const block = view.lineBlockAt(line[0].from);
|
|
388
|
+
const withinRow = (wanted - index) * block.height;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Set outright rather than nudged by a difference from where the editor is
|
|
392
|
+
* now. `lineBlockAt` measures from the top of the document, and the
|
|
393
|
+
* document begins `paddingTop` into the scroller's content, so that sum is
|
|
394
|
+
* already the position that puts the row against the top of the viewport.
|
|
395
|
+
* Working it out as a difference meant reading the current scroll offset,
|
|
396
|
+
* the scroller's box and the document's box — three measurements of the
|
|
397
|
+
* present, to arrive somewhere that never depended on it. The scroller
|
|
398
|
+
* clamps the result at both ends by itself.
|
|
399
|
+
*/
|
|
400
|
+
view.scrollDOM.scrollTop =
|
|
401
|
+
view.documentPadding.top + block.top + withinRow - offset;
|
|
402
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { DiagnosticState, diagnostics } from "./diagnostics.js";
|
|
|
6
6
|
import { SelectionState, selections } from "./selections.js";
|
|
7
7
|
import { TextState, text } from "./text.js";
|
|
8
8
|
import { LinesState } from "./LinesState.js";
|
|
9
|
+
import { readGeometry, setRowHeight } from "./geometry.js";
|
|
9
10
|
import crelt from "crelt";
|
|
10
11
|
import { GUTTER_WIDTH, drawLineGutter } from "./Gutters.js";
|
|
11
12
|
|
|
@@ -144,7 +145,7 @@ const minimapClass = ViewPlugin.fromClass(
|
|
|
144
145
|
this.text.update(update);
|
|
145
146
|
this.selection.update(update);
|
|
146
147
|
this.diagnostic.update(update);
|
|
147
|
-
this.render();
|
|
148
|
+
this.render(update);
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -161,7 +162,12 @@ const minimapClass = ViewPlugin.fromClass(
|
|
|
161
162
|
return maxWidth;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
/**
|
|
166
|
+
* `token` names the moment being drawn, and is passed on to
|
|
167
|
+
* `readGeometry` so that the overlay, which draws the same moment,
|
|
168
|
+
* measures it only once between them.
|
|
169
|
+
*/
|
|
170
|
+
render(token?: object) {
|
|
165
171
|
// If we don't have elements to draw to exit early
|
|
166
172
|
if (!this.dom || !this.canvas || !this.inner) {
|
|
167
173
|
return;
|
|
@@ -215,10 +221,12 @@ const minimapClass = ViewPlugin.fromClass(
|
|
|
215
221
|
|
|
216
222
|
/* We need to get the correct font dimensions before this to measure characters */
|
|
217
223
|
const { charWidth, lineHeight } = this.text.measure(context);
|
|
224
|
+
setRowHeight(this.view, lineHeight);
|
|
218
225
|
|
|
219
226
|
let { startIndex, endIndex, offsetY } = this.canvasStartAndEndIndex(
|
|
220
227
|
context,
|
|
221
228
|
lineHeight,
|
|
229
|
+
token,
|
|
222
230
|
);
|
|
223
231
|
|
|
224
232
|
const gutters = this.view.state.facet(Config).gutters;
|
|
@@ -261,33 +269,39 @@ const minimapClass = ViewPlugin.fromClass(
|
|
|
261
269
|
private canvasStartAndEndIndex(
|
|
262
270
|
context: CanvasRenderingContext2D,
|
|
263
271
|
lineHeight: number,
|
|
272
|
+
token?: object,
|
|
264
273
|
) {
|
|
265
|
-
const
|
|
266
|
-
this.view.documentPadding;
|
|
267
|
-
const pTop = rawTop / Scale.SizeRatio;
|
|
268
|
-
const pBottom = rawBottom / Scale.SizeRatio;
|
|
269
|
-
|
|
274
|
+
const pTop = this.view.documentPadding.top / Scale.SizeRatio;
|
|
270
275
|
const canvasHeight = context.canvas.height;
|
|
271
|
-
const { clientHeight, scrollHeight, scrollTop } =
|
|
272
|
-
this.view.scrollDOM;
|
|
273
|
-
let scrollPercent = scrollTop / (scrollHeight - clientHeight);
|
|
274
|
-
if (isNaN(scrollPercent)) {
|
|
275
|
-
scrollPercent = 0;
|
|
276
|
-
}
|
|
277
276
|
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
/**
|
|
278
|
+
* The rows are scrolled to wherever the overlay needs them: the two
|
|
279
|
+
* are laid out together so that the overlay covers the rows the
|
|
280
|
+
* editor is showing rather than an approximation of them.
|
|
281
|
+
*/
|
|
282
|
+
const canvasTop =
|
|
283
|
+
readGeometry(this.view, token).scrollTop *
|
|
284
|
+
Scale.PixelMultiplier;
|
|
280
285
|
|
|
281
|
-
|
|
282
|
-
0,
|
|
283
|
-
|
|
284
|
-
);
|
|
285
|
-
const offsetY = Math.max(0, pTop - canvasTop);
|
|
286
|
+
if (lineHeight <= 0) {
|
|
287
|
+
return { startIndex: 0, endIndex: 0, offsetY: 0 };
|
|
288
|
+
}
|
|
286
289
|
|
|
287
|
-
|
|
288
|
-
|
|
290
|
+
/**
|
|
291
|
+
* The first row to draw is the one the top edge falls inside, not
|
|
292
|
+
* the nearest one to it: rounding would leave every row up to half
|
|
293
|
+
* a row away from where the overlay expects it, and by a different
|
|
294
|
+
* amount at every scroll position. Whatever part of that row lies
|
|
295
|
+
* above the edge goes into `offsetY`, which is why it may be
|
|
296
|
+
* negative — the row is drawn from off the top of the canvas so the
|
|
297
|
+
* rest of it lands exactly where the overlay marks it.
|
|
298
|
+
*/
|
|
299
|
+
const startIndex = Math.max(
|
|
300
|
+
0,
|
|
301
|
+
Math.floor((canvasTop - pTop) / lineHeight),
|
|
289
302
|
);
|
|
290
|
-
const
|
|
303
|
+
const offsetY = pTop + startIndex * lineHeight - canvasTop;
|
|
304
|
+
const spaceForLines = Math.ceil(
|
|
291
305
|
(canvasHeight - offsetY) / lineHeight,
|
|
292
306
|
);
|
|
293
307
|
|
|
@@ -320,8 +334,8 @@ const minimapClass = ViewPlugin.fromClass(
|
|
|
320
334
|
},
|
|
321
335
|
{
|
|
322
336
|
eventHandlers: {
|
|
323
|
-
scroll() {
|
|
324
|
-
requestAnimationFrame(() => this.render());
|
|
337
|
+
scroll(event) {
|
|
338
|
+
requestAnimationFrame(() => this.render(event));
|
|
325
339
|
},
|
|
326
340
|
},
|
|
327
341
|
provide: (plugin) => {
|