⚡ Optimize minmap extension
This commit is contained in:
@@ -37,6 +37,7 @@ const Theme = EditorView.theme({
|
|||||||
overflowY: "hidden",
|
overflowY: "hidden",
|
||||||
"& canvas": {
|
"& canvas": {
|
||||||
display: "block",
|
display: "block",
|
||||||
|
willChange: "transform, opacity",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"& .cm-minimap-box-shadow": {
|
"& .cm-minimap-box-shadow": {
|
||||||
@@ -45,6 +46,7 @@ const Theme = EditorView.theme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const WIDTH_RATIO = 6;
|
const WIDTH_RATIO = 6;
|
||||||
|
type RenderReason = "scroll" | "data";
|
||||||
|
|
||||||
const minimapClass = ViewPlugin.fromClass(
|
const minimapClass = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
@@ -53,6 +55,9 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
private canvas: HTMLCanvasElement | undefined;
|
private canvas: HTMLCanvasElement | undefined;
|
||||||
private renderHandle: number | ReturnType<typeof setTimeout> | null = null;
|
private renderHandle: number | ReturnType<typeof setTimeout> | null = null;
|
||||||
private cancelRender: (() => void) | null = null;
|
private cancelRender: (() => void) | null = null;
|
||||||
|
private pendingScrollTop: number | null = null;
|
||||||
|
private lastRenderedScrollTop: number = -1;
|
||||||
|
private pendingRenderReason: RenderReason | null = null;
|
||||||
|
|
||||||
public text: TextState;
|
public text: TextState;
|
||||||
public selection: SelectionState;
|
public selection: SelectionState;
|
||||||
@@ -160,6 +165,11 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const effectiveScrollTop = this.pendingScrollTop ?? this.view.scrollDOM.scrollTop;
|
||||||
|
this.pendingScrollTop = null;
|
||||||
|
this.pendingRenderReason = null;
|
||||||
|
this.lastRenderedScrollTop = effectiveScrollTop;
|
||||||
|
|
||||||
this.text.beforeDraw();
|
this.text.beforeDraw();
|
||||||
|
|
||||||
this.updateBoxShadow();
|
this.updateBoxShadow();
|
||||||
@@ -185,7 +195,8 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
|
|
||||||
let { startIndex, endIndex, offsetY } = this.canvasStartAndEndIndex(
|
let { startIndex, endIndex, offsetY } = this.canvasStartAndEndIndex(
|
||||||
context,
|
context,
|
||||||
lineHeight
|
lineHeight,
|
||||||
|
effectiveScrollTop
|
||||||
);
|
);
|
||||||
|
|
||||||
const gutters = this.view.state.facet(Config).gutters;
|
const gutters = this.view.state.facet(Config).gutters;
|
||||||
@@ -216,15 +227,39 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
drawContext.offsetX += 2;
|
drawContext.offsetX += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.text.drawLine(drawContext, i + 1);
|
const lineNumber = i + 1;
|
||||||
this.selection.drawLine(drawContext, i + 1);
|
this.text.drawLine(drawContext, lineNumber);
|
||||||
this.diagnostic.drawLine(drawContext, i + 1);
|
this.selection.drawLine(drawContext, lineNumber);
|
||||||
|
|
||||||
|
if (this.diagnostic.has(lineNumber)) {
|
||||||
|
this.diagnostic.drawLine(drawContext, lineNumber);
|
||||||
|
}
|
||||||
|
|
||||||
offsetY += lineHeight;
|
offsetY += lineHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestRender() {
|
requestRender(reason: RenderReason = "data") {
|
||||||
|
if (reason === "scroll") {
|
||||||
|
const scrollTop = this.view.scrollDOM.scrollTop;
|
||||||
|
if (this.lastRenderedScrollTop === scrollTop && !this.pendingRenderReason) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.pendingRenderReason === "scroll" &&
|
||||||
|
this.pendingScrollTop === scrollTop
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pendingScrollTop = scrollTop;
|
||||||
|
} else {
|
||||||
|
this.pendingScrollTop = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason === "data" || this.pendingRenderReason === null) {
|
||||||
|
this.pendingRenderReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.renderHandle !== null) {
|
if (this.renderHandle !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -256,17 +291,21 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
this.cancelRender();
|
this.cancelRender();
|
||||||
this.renderHandle = null;
|
this.renderHandle = null;
|
||||||
this.cancelRender = null;
|
this.cancelRender = null;
|
||||||
|
this.pendingScrollTop = null;
|
||||||
|
this.pendingRenderReason = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private canvasStartAndEndIndex(
|
private canvasStartAndEndIndex(
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
lineHeight: number
|
lineHeight: number,
|
||||||
|
scrollTopOverride?: number
|
||||||
) {
|
) {
|
||||||
let { top: pTop, bottom: pBottom } = this.view.documentPadding;
|
let { top: pTop, bottom: pBottom } = this.view.documentPadding;
|
||||||
(pTop /= Scale.SizeRatio), (pBottom /= Scale.SizeRatio);
|
(pTop /= Scale.SizeRatio), (pBottom /= Scale.SizeRatio);
|
||||||
|
|
||||||
const canvasHeight = context.canvas.height;
|
const canvasHeight = context.canvas.height;
|
||||||
const { clientHeight, scrollHeight, scrollTop } = this.view.scrollDOM;
|
const { clientHeight, scrollHeight } = this.view.scrollDOM;
|
||||||
|
const scrollTop = scrollTopOverride ?? this.view.scrollDOM.scrollTop;
|
||||||
let scrollPercent = scrollTop / (scrollHeight - clientHeight);
|
let scrollPercent = scrollTop / (scrollHeight - clientHeight);
|
||||||
if (isNaN(scrollPercent)) {
|
if (isNaN(scrollPercent)) {
|
||||||
scrollPercent = 0;
|
scrollPercent = 0;
|
||||||
@@ -312,7 +351,7 @@ const minimapClass = ViewPlugin.fromClass(
|
|||||||
{
|
{
|
||||||
eventHandlers: {
|
eventHandlers: {
|
||||||
scroll() {
|
scroll() {
|
||||||
this.requestRender();
|
this.requestRender("scroll");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
provide: (plugin) => {
|
provide: (plugin) => {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export abstract class LineBasedState<TValue> {
|
|||||||
return this.map.get(lineNumber);
|
return this.map.get(lineNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public has(lineNumber: number): boolean {
|
||||||
|
return this.map.has(lineNumber);
|
||||||
|
}
|
||||||
|
|
||||||
protected set(lineNumber: number, value: TValue) {
|
protected set(lineNumber: number, value: TValue) {
|
||||||
this.map.set(lineNumber, value);
|
this.map.set(lineNumber, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ function computeLinesState(state: EditorState): Lines {
|
|||||||
const LinesState = StateField.define<Lines>({
|
const LinesState = StateField.define<Lines>({
|
||||||
create: (state) => computeLinesState(state),
|
create: (state) => computeLinesState(state),
|
||||||
update: (current, tr) => {
|
update: (current, tr) => {
|
||||||
|
const prevEnabled = tr.startState.facet(Config).enabled;
|
||||||
|
const nextEnabled = tr.state.facet(Config).enabled;
|
||||||
|
if (prevEnabled !== nextEnabled) {
|
||||||
|
return computeLinesState(tr.state);
|
||||||
|
}
|
||||||
|
|
||||||
if (foldsChanged([tr]) || tr.docChanged) {
|
if (foldsChanged([tr]) || tr.docChanged) {
|
||||||
return computeLinesState(tr.state);
|
return computeLinesState(tr.state);
|
||||||
}
|
}
|
||||||
@@ -93,3 +99,11 @@ function foldsChanged(transactions: readonly Transaction[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { foldsChanged, LinesState };
|
export { foldsChanged, LinesState };
|
||||||
|
|
||||||
|
export function getLinesSnapshot(state: EditorState): Lines {
|
||||||
|
const lines = state.field(LinesState);
|
||||||
|
if (lines.length || !state.facet(Config).enabled) {
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
return computeLinesState(state);
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const OverlayView = ViewPlugin.fromClass(
|
|||||||
|
|
||||||
private _isDragging: boolean = false;
|
private _isDragging: boolean = false;
|
||||||
private _dragStartY: number | undefined;
|
private _dragStartY: number | undefined;
|
||||||
|
private abortController: AbortController | undefined;
|
||||||
private readonly _boundMouseDown = (event: MouseEvent) => this.onMouseDown(event);
|
private readonly _boundMouseDown = (event: MouseEvent) => this.onMouseDown(event);
|
||||||
private readonly _boundMouseUp = (event: MouseEvent) => this.onMouseUp(event);
|
private readonly _boundMouseUp = (event: MouseEvent) => this.onMouseUp(event);
|
||||||
private readonly _boundMouseMove = (event: MouseEvent) => this.onMouseMove(event);
|
private readonly _boundMouseMove = (event: MouseEvent) => this.onMouseMove(event);
|
||||||
@@ -64,14 +65,17 @@ const OverlayView = ViewPlugin.fromClass(
|
|||||||
private create(view: EditorView) {
|
private create(view: EditorView) {
|
||||||
this.remove();
|
this.remove();
|
||||||
|
|
||||||
|
this.abortController = new AbortController();
|
||||||
|
const signal = this.abortController.signal;
|
||||||
|
|
||||||
this.container = crelt("div", { class: "cm-minimap-overlay-container" });
|
this.container = crelt("div", { class: "cm-minimap-overlay-container" });
|
||||||
this.dom = crelt("div", { class: "cm-minimap-overlay" });
|
this.dom = crelt("div", { class: "cm-minimap-overlay" });
|
||||||
this.container.appendChild(this.dom);
|
this.container.appendChild(this.dom);
|
||||||
|
|
||||||
// Attach event listeners for overlay
|
// Attach event listeners for overlay
|
||||||
this.container.addEventListener("mousedown", this._boundMouseDown);
|
this.container.addEventListener("mousedown", this._boundMouseDown, { signal });
|
||||||
window.addEventListener("mouseup", this._boundMouseUp);
|
window.addEventListener("mouseup", this._boundMouseUp, { signal });
|
||||||
window.addEventListener("mousemove", this._boundMouseMove);
|
window.addEventListener("mousemove", this._boundMouseMove, { signal });
|
||||||
|
|
||||||
// Attach the overlay elements to the minimap
|
// Attach the overlay elements to the minimap
|
||||||
const inner = view.dom.querySelector(".cm-minimap-inner");
|
const inner = view.dom.querySelector(".cm-minimap-inner");
|
||||||
@@ -86,10 +90,12 @@ const OverlayView = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private remove() {
|
private remove() {
|
||||||
|
if (this.abortController) {
|
||||||
|
this.abortController.abort();
|
||||||
|
this.abortController = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.container) {
|
if (this.container) {
|
||||||
this.container.removeEventListener("mousedown", this._boundMouseDown);
|
|
||||||
window.removeEventListener("mouseup", this._boundMouseUp);
|
|
||||||
window.removeEventListener("mousemove", this._boundMouseMove);
|
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
this.container = undefined;
|
this.container = undefined;
|
||||||
this.dom = undefined;
|
this.dom = undefined;
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
import { LineBasedState } from "./linebasedstate";
|
import { LineBasedState } from "./linebasedstate";
|
||||||
import { EditorView, ViewUpdate } from "@codemirror/view";
|
import { EditorView, ViewUpdate } from "@codemirror/view";
|
||||||
import { Lines, LinesState, foldsChanged } from "./linesState";
|
import { EditorState } from "@codemirror/state";
|
||||||
|
import { Lines, foldsChanged, getLinesSnapshot } from "./linesState";
|
||||||
import { DrawContext } from "./types";
|
import { DrawContext } from "./types";
|
||||||
import { Config } from "./config";
|
import { Config } from "./config";
|
||||||
import { lineLength, lineNumberAt, offsetWithinLine } from "./lineGeometry";
|
import { lineLength, lineNumberAt, offsetWithinLine } from "./lineGeometry";
|
||||||
|
|
||||||
type Selection = { from: number; to: number; extends: boolean };
|
type Selection = { from: number; to: number; extends: boolean };
|
||||||
type DrawInfo = { backgroundColor: string };
|
type DrawInfo = { backgroundColor: string };
|
||||||
|
type RangeInfo = {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
lineFrom: number;
|
||||||
|
lineTo: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_CACHED_LINES = 800;
|
||||||
|
|
||||||
export class SelectionState extends LineBasedState<Array<Selection>> {
|
export class SelectionState extends LineBasedState<Array<Selection>> {
|
||||||
private _drawInfo: DrawInfo | undefined;
|
private _drawInfo: DrawInfo | undefined;
|
||||||
private _themeClasses: string;
|
private _themeClasses: string;
|
||||||
|
private _rangeInfo: Array<RangeInfo> = [];
|
||||||
|
private _linesSnapshot: Lines = [];
|
||||||
|
|
||||||
public constructor(view: EditorView) {
|
public constructor(view: EditorView) {
|
||||||
super(view);
|
super(view);
|
||||||
@@ -53,40 +64,7 @@ export class SelectionState extends LineBasedState<Array<Selection>> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._themeClasses !== this.view.dom.classList.value) {
|
this.rebuild(update.state);
|
||||||
this._drawInfo = undefined;
|
|
||||||
this._themeClasses = this.view.dom.classList.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = update.state.field(LinesState);
|
|
||||||
const nextSelections = new Map<number, Array<Selection>>();
|
|
||||||
|
|
||||||
for (const range of update.state.selection.ranges) {
|
|
||||||
if (range.empty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startLine = lineNumberAt(lines, range.from);
|
|
||||||
const endLine = lineNumberAt(
|
|
||||||
lines,
|
|
||||||
Math.max(range.from, range.to - 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (startLine <= 0 || endLine <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.collectRangeSelections(
|
|
||||||
nextSelections,
|
|
||||||
lines,
|
|
||||||
range.from,
|
|
||||||
range.to,
|
|
||||||
startLine,
|
|
||||||
endLine
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.applySelectionDiff(nextSelections);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public drawLine(ctx: DrawContext, lineNumber: number) {
|
public drawLine(ctx: DrawContext, lineNumber: number) {
|
||||||
@@ -97,7 +75,7 @@ export class SelectionState extends LineBasedState<Array<Selection>> {
|
|||||||
offsetX: startOffsetX,
|
offsetX: startOffsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
} = ctx;
|
} = ctx;
|
||||||
const selections = this.get(lineNumber);
|
const selections = this.ensureSelections(lineNumber);
|
||||||
if (!selections) {
|
if (!selections) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -146,56 +124,6 @@ export class SelectionState extends LineBasedState<Array<Selection>> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectRangeSelections(
|
|
||||||
store: Map<number, Array<Selection>>,
|
|
||||||
lines: Lines,
|
|
||||||
rangeFrom: number,
|
|
||||||
rangeTo: number,
|
|
||||||
startLine: number,
|
|
||||||
endLine: number
|
|
||||||
) {
|
|
||||||
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
|
|
||||||
const spans = lines[lineNumber - 1];
|
|
||||||
if (!spans || spans.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const length = lineLength(spans);
|
|
||||||
const fromOffset =
|
|
||||||
lineNumber === startLine ? offsetWithinLine(rangeFrom, spans) : 0;
|
|
||||||
|
|
||||||
let toOffset =
|
|
||||||
lineNumber === endLine ? offsetWithinLine(rangeTo, spans) : length;
|
|
||||||
if (toOffset === fromOffset) {
|
|
||||||
toOffset = Math.min(length, fromOffset + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSpan = spans[spans.length - 1];
|
|
||||||
const spanEnd = lastSpan ? lastSpan.to : rangeTo;
|
|
||||||
const extendsLine =
|
|
||||||
lineNumber < endLine || (lineNumber === endLine && rangeTo > spanEnd);
|
|
||||||
|
|
||||||
const selections = this.ensureLineEntry(store, lineNumber);
|
|
||||||
this.appendSelection(selections, {
|
|
||||||
from: fromOffset,
|
|
||||||
to: toOffset,
|
|
||||||
extends: extendsLine,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureLineEntry(
|
|
||||||
store: Map<number, Array<Selection>>,
|
|
||||||
lineNumber: number
|
|
||||||
) {
|
|
||||||
let selections = store.get(lineNumber);
|
|
||||||
if (!selections) {
|
|
||||||
selections = [];
|
|
||||||
store.set(lineNumber, selections);
|
|
||||||
}
|
|
||||||
return selections;
|
|
||||||
}
|
|
||||||
|
|
||||||
private appendSelection(target: Array<Selection>, entry: Selection) {
|
private appendSelection(target: Array<Selection>, entry: Selection) {
|
||||||
const last = target[target.length - 1];
|
const last = target[target.length - 1];
|
||||||
if (last && entry.from <= last.to) {
|
if (last && entry.from <= last.to) {
|
||||||
@@ -208,40 +136,109 @@ export class SelectionState extends LineBasedState<Array<Selection>> {
|
|||||||
}
|
}
|
||||||
target.push(entry);
|
target.push(entry);
|
||||||
}
|
}
|
||||||
|
private rebuild(state: EditorState) {
|
||||||
private applySelectionDiff(nextMap: Map<number, Array<Selection>>) {
|
if (this._themeClasses !== this.view.dom.classList.value) {
|
||||||
if (nextMap.size === 0 && this.map.size === 0) {
|
this._drawInfo = undefined;
|
||||||
return;
|
this._themeClasses = this.view.dom.classList.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of Array.from(this.map.keys())) {
|
this._linesSnapshot = getLinesSnapshot(state);
|
||||||
if (!nextMap.has(key)) {
|
this._rangeInfo = this.buildRangeInfo(state, this._linesSnapshot);
|
||||||
this.map.delete(key);
|
this.map.clear();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [lineNumber, selections] of nextMap) {
|
|
||||||
const existing = this.map.get(lineNumber);
|
|
||||||
if (!existing || !this.areSelectionsEqual(existing, selections)) {
|
|
||||||
this.map.set(lineNumber, selections);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private areSelectionsEqual(a: Array<Selection>, b: Array<Selection>) {
|
private buildRangeInfo(state: EditorState, lines: Lines) {
|
||||||
if (a.length !== b.length) {
|
const info: Array<RangeInfo> = [];
|
||||||
return false;
|
for (const range of state.selection.ranges) {
|
||||||
}
|
if (range.empty) {
|
||||||
for (let i = 0; i < a.length; i++) {
|
continue;
|
||||||
if (
|
|
||||||
a[i].from !== b[i].from ||
|
|
||||||
a[i].to !== b[i].to ||
|
|
||||||
a[i].extends !== b[i].extends
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startLine = lineNumberAt(lines, range.from);
|
||||||
|
const endLine = lineNumberAt(lines, Math.max(range.from, range.to - 1));
|
||||||
|
if (startLine <= 0 || endLine <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.push({
|
||||||
|
from: range.from,
|
||||||
|
to: range.to,
|
||||||
|
lineFrom: startLine,
|
||||||
|
lineTo: endLine,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureSelections(lineNumber: number) {
|
||||||
|
const cached = this.get(lineNumber);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computed = this.buildSelectionsForLine(lineNumber);
|
||||||
|
if (!computed || computed.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.map.has(lineNumber)) {
|
||||||
|
this.map.delete(lineNumber);
|
||||||
|
}
|
||||||
|
this.map.set(lineNumber, computed);
|
||||||
|
while (this.map.size > MAX_CACHED_LINES) {
|
||||||
|
const oldest = this.map.keys().next();
|
||||||
|
if (oldest.done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.map.delete(oldest.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSelectionsForLine(lineNumber: number) {
|
||||||
|
const spans = this._linesSnapshot[lineNumber - 1];
|
||||||
|
if (!spans || spans.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevant = this._rangeInfo.filter(
|
||||||
|
(info) => lineNumber >= info.lineFrom && lineNumber <= info.lineTo
|
||||||
|
);
|
||||||
|
if (!relevant.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selections: Array<Selection> = [];
|
||||||
|
for (const range of relevant) {
|
||||||
|
const length = lineLength(spans);
|
||||||
|
const fromOffset =
|
||||||
|
lineNumber === range.lineFrom
|
||||||
|
? offsetWithinLine(range.from, spans)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
let toOffset =
|
||||||
|
lineNumber === range.lineTo
|
||||||
|
? offsetWithinLine(range.to, spans)
|
||||||
|
: length;
|
||||||
|
if (toOffset === fromOffset) {
|
||||||
|
toOffset = Math.min(length, fromOffset + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSpan = spans[spans.length - 1];
|
||||||
|
const spanEnd = lastSpan ? lastSpan.to : range.to;
|
||||||
|
const extendsLine =
|
||||||
|
lineNumber < range.lineTo ||
|
||||||
|
(lineNumber === range.lineTo && range.to > spanEnd);
|
||||||
|
|
||||||
|
this.appendSelection(selections, {
|
||||||
|
from: fromOffset,
|
||||||
|
to: toOffset,
|
||||||
|
extends: extendsLine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return selections;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -404,7 +404,13 @@ export class TextState extends LineBasedState<Array<TagSpan>> {
|
|||||||
|
|
||||||
// Get style information and store it
|
// Get style information and store it
|
||||||
const style = window.getComputedStyle(mockToken);
|
const style = window.getComputedStyle(mockToken);
|
||||||
const lineHeight = parseFloat(style.lineHeight) / Scale.SizeRatio;
|
const rawLineHeight = parseFloat(style.lineHeight);
|
||||||
|
const fallbackLineHeight = parseFloat(style.fontSize) || this.view.defaultLineHeight;
|
||||||
|
const resolvedLineHeight =
|
||||||
|
Number.isFinite(rawLineHeight) && rawLineHeight > 0
|
||||||
|
? rawLineHeight
|
||||||
|
: fallbackLineHeight;
|
||||||
|
const lineHeight = Math.max(1, resolvedLineHeight / Scale.SizeRatio);
|
||||||
const result = {
|
const result = {
|
||||||
color: style.color,
|
color: style.color,
|
||||||
font: `${style.fontStyle} ${style.fontWeight} ${lineHeight}px ${style.fontFamily}`,
|
font: `${style.fontStyle} ${style.fontWeight} ${lineHeight}px ${style.fontFamily}`,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type LineBitmap = {
|
|||||||
export class LineRenderer {
|
export class LineRenderer {
|
||||||
private readonly _lineVersions = new Map<number, number>();
|
private readonly _lineVersions = new Map<number, number>();
|
||||||
private readonly _lineCache = new Map<number, LineBitmap>();
|
private readonly _lineCache = new Map<number, LineBitmap>();
|
||||||
|
private static readonly MAX_CACHE_LINES = 2000;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly glyphAtlas: GlyphAtlas,
|
private readonly glyphAtlas: GlyphAtlas,
|
||||||
@@ -28,6 +29,7 @@ export class LineRenderer {
|
|||||||
const version = (this._lineVersions.get(lineNumber) ?? 0) + 1;
|
const version = (this._lineVersions.get(lineNumber) ?? 0) + 1;
|
||||||
this._lineVersions.set(lineNumber, version);
|
this._lineVersions.set(lineNumber, version);
|
||||||
this._lineCache.delete(lineNumber);
|
this._lineCache.delete(lineNumber);
|
||||||
|
this.trimCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public markAllChanged() {
|
public markAllChanged() {
|
||||||
@@ -42,6 +44,7 @@ export class LineRenderer {
|
|||||||
this._lineCache.delete(key);
|
this._lineCache.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.trimCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public drawLine(
|
public drawLine(
|
||||||
@@ -170,9 +173,19 @@ export class LineRenderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._lineCache.set(lineNumber, entry);
|
this._lineCache.set(lineNumber, entry);
|
||||||
|
this.trimCache();
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private trimCache() {
|
||||||
|
while (this._lineCache.size > LineRenderer.MAX_CACHE_LINES) {
|
||||||
|
const oldest = this._lineCache.keys().next();
|
||||||
|
if (oldest.done) break;
|
||||||
|
this._lineCache.delete(oldest.value);
|
||||||
|
this._lineVersions.delete(oldest.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private paintSpans(
|
private paintSpans(
|
||||||
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||||
spans: Array<TagSpan>,
|
spans: Array<TagSpan>,
|
||||||
|
|||||||
Reference in New Issue
Block a user