@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/src/index.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { Facet } from "@codemirror/state";
|
|
2
|
+
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
|
3
|
+
import { Overlay } from "./Overlay.js";
|
|
4
|
+
import { Config, Options, Scale } from "./Config.js";
|
|
5
|
+
import { DiagnosticState, diagnostics } from "./diagnostics.js";
|
|
6
|
+
import { SelectionState, selections } from "./selections.js";
|
|
7
|
+
import { TextState, text } from "./text.js";
|
|
8
|
+
import { LinesState } from "./LinesState.js";
|
|
9
|
+
import crelt from "crelt";
|
|
10
|
+
import { GUTTER_WIDTH, drawLineGutter } from "./Gutters.js";
|
|
11
|
+
|
|
12
|
+
const Theme = EditorView.theme({
|
|
13
|
+
"&": {
|
|
14
|
+
height: "100%",
|
|
15
|
+
overflowY: "auto",
|
|
16
|
+
},
|
|
17
|
+
"& .cm-minimap-gutter": {
|
|
18
|
+
borderRight: 0,
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
left: "unset",
|
|
21
|
+
position: "sticky",
|
|
22
|
+
right: 0,
|
|
23
|
+
top: 0,
|
|
24
|
+
},
|
|
25
|
+
"& .cm-minimap-autohide": {
|
|
26
|
+
opacity: 0.0,
|
|
27
|
+
transition: "opacity 0.3s",
|
|
28
|
+
},
|
|
29
|
+
"& .cm-minimap-autohide:hover": {
|
|
30
|
+
opacity: 1.0,
|
|
31
|
+
},
|
|
32
|
+
"& .cm-minimap-inner": {
|
|
33
|
+
height: "100%",
|
|
34
|
+
position: "absolute",
|
|
35
|
+
right: 0,
|
|
36
|
+
top: 0,
|
|
37
|
+
overflowY: "hidden",
|
|
38
|
+
"& canvas": {
|
|
39
|
+
display: "block",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
"& .cm-minimap-box-shadow": {
|
|
43
|
+
boxShadow: "12px 0px 20px 5px #6c6c6c",
|
|
44
|
+
},
|
|
45
|
+
// Suppresses the scrollbar's appearance only; the scroller keeps its
|
|
46
|
+
// overflow, so wheel, trackpad, keyboard and programmatic scrolling all
|
|
47
|
+
// continue to work.
|
|
48
|
+
// Lives on the scroller rather than the editor root: `themeChanged`
|
|
49
|
+
// fingerprints the root's class list, so toggling a class there would read
|
|
50
|
+
// as a theme change and needlessly drop the font metric cache.
|
|
51
|
+
"& .cm-scroller.cm-minimap-hide-scrollbar": {
|
|
52
|
+
scrollbarWidth: "none",
|
|
53
|
+
"&::-webkit-scrollbar": {
|
|
54
|
+
display: "none",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const HIDE_SCROLLBAR_CLASS = "cm-minimap-hide-scrollbar";
|
|
60
|
+
|
|
61
|
+
const WIDTH_RATIO = 6;
|
|
62
|
+
|
|
63
|
+
const minimapClass = ViewPlugin.fromClass(
|
|
64
|
+
class {
|
|
65
|
+
private dom: HTMLElement | undefined;
|
|
66
|
+
private inner: HTMLElement | undefined;
|
|
67
|
+
private canvas: HTMLCanvasElement | undefined;
|
|
68
|
+
|
|
69
|
+
public text: TextState;
|
|
70
|
+
public selection: SelectionState;
|
|
71
|
+
public diagnostic: DiagnosticState;
|
|
72
|
+
|
|
73
|
+
public constructor(private view: EditorView) {
|
|
74
|
+
this.text = text(view);
|
|
75
|
+
this.selection = selections(view);
|
|
76
|
+
this.diagnostic = diagnostics(view);
|
|
77
|
+
|
|
78
|
+
if (view.state.facet(showMinimap)) {
|
|
79
|
+
this.create(view);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private create(view: EditorView) {
|
|
84
|
+
const config = view.state.facet(showMinimap);
|
|
85
|
+
if (!config) {
|
|
86
|
+
throw Error("Expected nonnull");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.inner = crelt("div", { class: "cm-minimap-inner" });
|
|
90
|
+
this.canvas = crelt("canvas");
|
|
91
|
+
|
|
92
|
+
this.dom = config.create(view).dom;
|
|
93
|
+
this.dom.classList.add("cm-gutters");
|
|
94
|
+
this.dom.classList.add("cm-minimap-gutter");
|
|
95
|
+
|
|
96
|
+
this.inner.appendChild(this.canvas);
|
|
97
|
+
this.dom.appendChild(this.inner);
|
|
98
|
+
|
|
99
|
+
// For now let's keep this same behavior. We might want to change
|
|
100
|
+
// this in the future and have the extension figure out how to mount.
|
|
101
|
+
// Or expose some more generic right gutter api and use that
|
|
102
|
+
this.view.scrollDOM.insertBefore(
|
|
103
|
+
this.dom,
|
|
104
|
+
this.view.contentDOM.nextSibling,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
for (const key in this.view.state.facet(Config).eventHandlers) {
|
|
108
|
+
const handler =
|
|
109
|
+
this.view.state.facet(Config).eventHandlers[key];
|
|
110
|
+
if (handler) {
|
|
111
|
+
this.dom.addEventListener(key, (e) =>
|
|
112
|
+
handler(e, this.view),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (config.autohide) {
|
|
118
|
+
this.dom.classList.add("cm-minimap-autohide");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private remove() {
|
|
123
|
+
this.view.scrollDOM.classList.remove(HIDE_SCROLLBAR_CLASS);
|
|
124
|
+
|
|
125
|
+
if (this.dom) {
|
|
126
|
+
this.dom.remove();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
update(update: ViewUpdate) {
|
|
131
|
+
const prev = update.startState.facet(showMinimap);
|
|
132
|
+
const now = update.state.facet(showMinimap);
|
|
133
|
+
|
|
134
|
+
if (prev && !now) {
|
|
135
|
+
this.remove();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!prev && now) {
|
|
140
|
+
this.create(update.view);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (now) {
|
|
144
|
+
this.text.update(update);
|
|
145
|
+
this.selection.update(update);
|
|
146
|
+
this.diagnostic.update(update);
|
|
147
|
+
this.render();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getWidth(): number {
|
|
152
|
+
const maxWidth = this.view.state.facet(Config).width;
|
|
153
|
+
const editorWidth = this.view.dom.clientWidth;
|
|
154
|
+
|
|
155
|
+
// Shrink proportionally rather than letting the minimap take over
|
|
156
|
+
// a narrow editor.
|
|
157
|
+
if (editorWidth <= maxWidth * WIDTH_RATIO) {
|
|
158
|
+
return maxWidth * (editorWidth / (maxWidth * WIDTH_RATIO));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return maxWidth;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
render() {
|
|
165
|
+
// If we don't have elements to draw to exit early
|
|
166
|
+
if (!this.dom || !this.canvas || !this.inner) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.updateBoxShadow();
|
|
171
|
+
this.view.scrollDOM.classList.toggle(
|
|
172
|
+
HIDE_SCROLLBAR_CLASS,
|
|
173
|
+
this.view.state.facet(Config).hideScrollbar,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const width = this.getWidth();
|
|
177
|
+
const domHeight = this.view.dom.getBoundingClientRect().height;
|
|
178
|
+
|
|
179
|
+
this.dom.style.width = width + "px";
|
|
180
|
+
this.canvas.style.maxWidth = width + "px";
|
|
181
|
+
this.inner.style.minHeight = domHeight + "px";
|
|
182
|
+
this.canvas.style.height = domHeight + "px";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Assigning to `width`/`height` reallocates the backing buffer and
|
|
186
|
+
* resets every canvas property, so only do it on an actual size
|
|
187
|
+
* change. The reset doubles as the clear when it does happen.
|
|
188
|
+
*/
|
|
189
|
+
const canvasWidth = width * Scale.PixelMultiplier;
|
|
190
|
+
const canvasHeight = domHeight * Scale.PixelMultiplier;
|
|
191
|
+
const resized =
|
|
192
|
+
this.canvas.width !== canvasWidth ||
|
|
193
|
+
this.canvas.height !== canvasHeight;
|
|
194
|
+
|
|
195
|
+
if (resized) {
|
|
196
|
+
this.canvas.width = canvasWidth;
|
|
197
|
+
this.canvas.height = canvasHeight;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const context = this.canvas.getContext("2d");
|
|
201
|
+
if (!context) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!resized) {
|
|
206
|
+
context.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Draw layers leave `globalAlpha` faded behind them. Resizing used
|
|
211
|
+
* to reset it as a side effect of reallocating the canvas; now that
|
|
212
|
+
* the resize is conditional, reset it explicitly instead.
|
|
213
|
+
*/
|
|
214
|
+
context.globalAlpha = 1;
|
|
215
|
+
|
|
216
|
+
/* We need to get the correct font dimensions before this to measure characters */
|
|
217
|
+
const { charWidth, lineHeight } = this.text.measure(context);
|
|
218
|
+
|
|
219
|
+
let { startIndex, endIndex, offsetY } = this.canvasStartAndEndIndex(
|
|
220
|
+
context,
|
|
221
|
+
lineHeight,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const gutters = this.view.state.facet(Config).gutters;
|
|
225
|
+
const lineCount = this.view.state.field(LinesState).length;
|
|
226
|
+
|
|
227
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
228
|
+
if (i >= lineCount) break;
|
|
229
|
+
|
|
230
|
+
const drawContext = {
|
|
231
|
+
offsetX: 0,
|
|
232
|
+
offsetY,
|
|
233
|
+
context,
|
|
234
|
+
lineHeight,
|
|
235
|
+
charWidth,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (gutters.length) {
|
|
239
|
+
/* Small leading buffer */
|
|
240
|
+
drawContext.offsetX += 2;
|
|
241
|
+
|
|
242
|
+
for (const gutter of gutters) {
|
|
243
|
+
drawLineGutter(gutter, drawContext, i + 1);
|
|
244
|
+
drawContext.offsetX += GUTTER_WIDTH;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Small trailing buffer */
|
|
248
|
+
drawContext.offsetX += 2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.text.drawLine(drawContext, i + 1);
|
|
252
|
+
this.selection.drawLine(drawContext, i + 1);
|
|
253
|
+
this.diagnostic.drawLine(drawContext, i + 1);
|
|
254
|
+
|
|
255
|
+
offsetY += lineHeight;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
context.restore();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private canvasStartAndEndIndex(
|
|
262
|
+
context: CanvasRenderingContext2D,
|
|
263
|
+
lineHeight: number,
|
|
264
|
+
) {
|
|
265
|
+
const { top: rawTop, bottom: rawBottom } =
|
|
266
|
+
this.view.documentPadding;
|
|
267
|
+
const pTop = rawTop / Scale.SizeRatio;
|
|
268
|
+
const pBottom = rawBottom / Scale.SizeRatio;
|
|
269
|
+
|
|
270
|
+
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
|
+
|
|
278
|
+
const lineCount = this.view.state.field(LinesState).length;
|
|
279
|
+
const totalHeight = pTop + pBottom + lineCount * lineHeight;
|
|
280
|
+
|
|
281
|
+
const canvasTop = Math.max(
|
|
282
|
+
0,
|
|
283
|
+
scrollPercent * (totalHeight - canvasHeight),
|
|
284
|
+
);
|
|
285
|
+
const offsetY = Math.max(0, pTop - canvasTop);
|
|
286
|
+
|
|
287
|
+
const startIndex = Math.round(
|
|
288
|
+
Math.max(0, canvasTop - pTop) / lineHeight,
|
|
289
|
+
);
|
|
290
|
+
const spaceForLines = Math.round(
|
|
291
|
+
(canvasHeight - offsetY) / lineHeight,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
startIndex,
|
|
296
|
+
endIndex: startIndex + spaceForLines,
|
|
297
|
+
offsetY,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private updateBoxShadow() {
|
|
302
|
+
if (!this.canvas) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const { clientWidth, scrollWidth, scrollLeft } =
|
|
307
|
+
this.view.scrollDOM;
|
|
308
|
+
|
|
309
|
+
if (clientWidth + scrollLeft < scrollWidth) {
|
|
310
|
+
this.canvas.classList.add("cm-minimap-box-shadow");
|
|
311
|
+
} else {
|
|
312
|
+
this.canvas.classList.remove("cm-minimap-box-shadow");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
destroy() {
|
|
317
|
+
this.text.destroy();
|
|
318
|
+
this.remove();
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
eventHandlers: {
|
|
323
|
+
scroll() {
|
|
324
|
+
requestAnimationFrame(() => this.render());
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
provide: (plugin) => {
|
|
328
|
+
return EditorView.scrollMargins.of((view) => {
|
|
329
|
+
const width = view.plugin(plugin)?.getWidth();
|
|
330
|
+
if (!width) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { right: width };
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
export interface MinimapConfig extends Omit<Options, "enabled"> {
|
|
341
|
+
/**
|
|
342
|
+
* A function that creates the element that contains the minimap
|
|
343
|
+
*/
|
|
344
|
+
create: (view: EditorView) => { dom: HTMLElement };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Facet used to show a minimap in the right gutter of the editor using the
|
|
349
|
+
* provided configuration.
|
|
350
|
+
*
|
|
351
|
+
* If you return `null`, a minimap will not be shown.
|
|
352
|
+
*/
|
|
353
|
+
const showMinimap = Facet.define<MinimapConfig | null, MinimapConfig | null>({
|
|
354
|
+
combine: (c) => c.find((o) => o !== null) ?? null,
|
|
355
|
+
enables: (f) => {
|
|
356
|
+
return [
|
|
357
|
+
[
|
|
358
|
+
Config.compute([f], (s) => s.facet(f)),
|
|
359
|
+
Theme,
|
|
360
|
+
LinesState,
|
|
361
|
+
minimapClass, // TODO, codemirror-ify this one better
|
|
362
|
+
Overlay,
|
|
363
|
+
],
|
|
364
|
+
];
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
export { showMinimap };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
|
|
3
|
+
// TODO: renamed this file because something's weird with codemirror build
|
|
4
|
+
|
|
5
|
+
export abstract class LineBasedState<TValue> {
|
|
6
|
+
protected map: Map<number, TValue>;
|
|
7
|
+
protected view: EditorView;
|
|
8
|
+
|
|
9
|
+
public constructor(view: EditorView) {
|
|
10
|
+
this.map = new Map();
|
|
11
|
+
this.view = view;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public get(lineNumber: number): TValue | undefined {
|
|
15
|
+
return this.map.get(lineNumber);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected set(lineNumber: number, value: TValue) {
|
|
19
|
+
this.map.set(lineNumber, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { LineBasedState } from "./linebasedstate.js";
|
|
2
|
+
import { EditorView, ViewUpdate } from "@codemirror/view";
|
|
3
|
+
import { LinesState, foldsChanged } from "./LinesState.js";
|
|
4
|
+
import { DrawContext } from "./types.js";
|
|
5
|
+
import { Config } from "./Config.js";
|
|
6
|
+
|
|
7
|
+
type Selection = { from: number; to: number; extends: boolean };
|
|
8
|
+
type DrawInfo = { backgroundColor: string };
|
|
9
|
+
|
|
10
|
+
export class SelectionState extends LineBasedState<Array<Selection>> {
|
|
11
|
+
private _drawInfo: DrawInfo | undefined;
|
|
12
|
+
private _themeClasses: string;
|
|
13
|
+
|
|
14
|
+
public constructor(view: EditorView) {
|
|
15
|
+
super(view);
|
|
16
|
+
|
|
17
|
+
this.getDrawInfo();
|
|
18
|
+
this._themeClasses = view.dom.classList.value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private shouldUpdate(update: ViewUpdate) {
|
|
22
|
+
// If the minimap is disabled
|
|
23
|
+
if (!update.state.facet(Config).enabled) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If the doc changed
|
|
28
|
+
if (update.docChanged) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If the selection changed
|
|
33
|
+
if (update.selectionSet) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If the theme changed
|
|
38
|
+
if (this._themeClasses !== this.view.dom.classList.value) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If the folds changed
|
|
43
|
+
if (foldsChanged(update.transactions)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public update(update: ViewUpdate) {
|
|
51
|
+
if (!this.shouldUpdate(update)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.map.clear();
|
|
56
|
+
|
|
57
|
+
/* If class list has changed, clear and recalculate the selection style */
|
|
58
|
+
if (this._themeClasses !== this.view.dom.classList.value) {
|
|
59
|
+
this._drawInfo = undefined;
|
|
60
|
+
this._themeClasses = this.view.dom.classList.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { ranges } = update.state.selection;
|
|
64
|
+
|
|
65
|
+
let selectionIndex = 0;
|
|
66
|
+
for (const [index, line] of update.state.field(LinesState).entries()) {
|
|
67
|
+
const selections: Array<Selection> = [];
|
|
68
|
+
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const span of line) {
|
|
71
|
+
do {
|
|
72
|
+
// We've already processed all selections
|
|
73
|
+
if (selectionIndex >= ranges.length) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The next selection begins after this span
|
|
78
|
+
if (span.to < ranges[selectionIndex].from) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Ignore 0-length selections
|
|
83
|
+
if (
|
|
84
|
+
ranges[selectionIndex].from ===
|
|
85
|
+
ranges[selectionIndex].to
|
|
86
|
+
) {
|
|
87
|
+
selectionIndex++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build the selection for the current span
|
|
92
|
+
const range = ranges[selectionIndex];
|
|
93
|
+
const selection = {
|
|
94
|
+
from:
|
|
95
|
+
offset +
|
|
96
|
+
Math.max(span.from, range.from) -
|
|
97
|
+
span.from,
|
|
98
|
+
to: offset + Math.min(span.to, range.to) - span.from,
|
|
99
|
+
extends: range.to > span.to,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const lastSelection = selections.slice(-1)[0];
|
|
103
|
+
if (lastSelection && lastSelection.to === selection.from) {
|
|
104
|
+
// The selection in this span may just be a continuation of the
|
|
105
|
+
// selection in the previous span
|
|
106
|
+
|
|
107
|
+
// Adjust `to` depending on if we're in a folded span
|
|
108
|
+
let { to } = selection;
|
|
109
|
+
if (span.folded && selection.extends) {
|
|
110
|
+
to = selection.from + 1;
|
|
111
|
+
} else if (span.folded && !selection.extends) {
|
|
112
|
+
to = lastSelection.to;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
selections[selections.length - 1] = {
|
|
116
|
+
...lastSelection,
|
|
117
|
+
to,
|
|
118
|
+
extends: selection.extends,
|
|
119
|
+
};
|
|
120
|
+
} else if (!span.folded) {
|
|
121
|
+
// It's a new selection; if we're not in a folded span we
|
|
122
|
+
// should push it onto the stack
|
|
123
|
+
selections.push(selection);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If the selection doesn't end in this span, break out of the loop
|
|
127
|
+
if (selection.extends) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Otherwise, move to the next selection
|
|
132
|
+
selectionIndex++;
|
|
133
|
+
} while (
|
|
134
|
+
selectionIndex < ranges.length &&
|
|
135
|
+
span.to >= ranges[selectionIndex].from
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
offset += span.folded ? 1 : span.to - span.from;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If we don't have any selections on this line, we don't need to store anything
|
|
142
|
+
if (selections.length === 0) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Lines are indexed beginning at 1 instead of 0
|
|
147
|
+
const lineNumber = index + 1;
|
|
148
|
+
this.map.set(lineNumber, selections);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public drawLine(ctx: DrawContext, lineNumber: number) {
|
|
153
|
+
const {
|
|
154
|
+
context,
|
|
155
|
+
lineHeight,
|
|
156
|
+
charWidth,
|
|
157
|
+
offsetX: startOffsetX,
|
|
158
|
+
offsetY,
|
|
159
|
+
} = ctx;
|
|
160
|
+
const selections = this.get(lineNumber);
|
|
161
|
+
if (!selections) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const selection of selections) {
|
|
166
|
+
const offsetX = startOffsetX + selection.from * charWidth;
|
|
167
|
+
const textWidth = (selection.to - selection.from) * charWidth;
|
|
168
|
+
const fullWidth = context.canvas.width - offsetX;
|
|
169
|
+
|
|
170
|
+
if (selection.extends) {
|
|
171
|
+
// Draw the full width rectangle in the background
|
|
172
|
+
context.globalAlpha = 0.65;
|
|
173
|
+
context.beginPath();
|
|
174
|
+
context.rect(offsetX, offsetY, fullWidth, lineHeight);
|
|
175
|
+
context.fillStyle = this.getDrawInfo().backgroundColor;
|
|
176
|
+
context.fill();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Draw text selection rectangle in the foreground
|
|
180
|
+
context.globalAlpha = 1;
|
|
181
|
+
context.beginPath();
|
|
182
|
+
context.rect(offsetX, offsetY, textWidth, lineHeight);
|
|
183
|
+
context.fillStyle = this.getDrawInfo().backgroundColor;
|
|
184
|
+
context.fill();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private getDrawInfo(): DrawInfo {
|
|
189
|
+
if (this._drawInfo) {
|
|
190
|
+
return this._drawInfo;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create a mock selection
|
|
194
|
+
const mockToken = document.createElement("span");
|
|
195
|
+
mockToken.setAttribute("class", "cm-selectionBackground");
|
|
196
|
+
this.view.dom.appendChild(mockToken);
|
|
197
|
+
|
|
198
|
+
// Get style information
|
|
199
|
+
const style = window.getComputedStyle(mockToken);
|
|
200
|
+
const result = { backgroundColor: style.backgroundColor };
|
|
201
|
+
|
|
202
|
+
// Store the result for the next update
|
|
203
|
+
this._drawInfo = result;
|
|
204
|
+
this.view.dom.removeChild(mockToken);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function selections(view: EditorView): SelectionState {
|
|
211
|
+
return new SelectionState(view);
|
|
212
|
+
}
|