@singapore-editor/scope-lines 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.
@@ -0,0 +1,302 @@
1
+ import "./style.css";
2
+ import { createEditorSecondaryTextView } from "@singapore-editor/core/secondary-views";
3
+ //#region src/stickyScroll.ts
4
+ var DEFAULT_MAX_LINE_COUNT = 5;
5
+ var EMPTY_HEADER = {
6
+ rows: [],
7
+ key: "",
8
+ top: 0,
9
+ bottom: 0,
10
+ stackHeight: 0
11
+ };
12
+ function createStickyScrollPlugin(options = {}) {
13
+ const resolved = resolveStickyScrollOptions(options);
14
+ return {
15
+ name: "sticky-scroll",
16
+ activate(context) {
17
+ return context.registerViewContribution({ createContribution: (contributionContext) => createStickyScrollContribution(contributionContext, resolved) });
18
+ }
19
+ };
20
+ }
21
+ function createStickyScrollContribution(context, options) {
22
+ if (!options.enabled) return null;
23
+ return new StickyScrollContribution(context, options);
24
+ }
25
+ var StickyScrollContribution = class {
26
+ inputs = [
27
+ "content",
28
+ "tokens",
29
+ "viewport",
30
+ "layout"
31
+ ];
32
+ context;
33
+ options;
34
+ root;
35
+ lineView = null;
36
+ appliedText = "";
37
+ appliedTokens = null;
38
+ appliedTheme = null;
39
+ appliedMetrics = null;
40
+ contentKey = "";
41
+ layoutKey = "";
42
+ constructor(context, options) {
43
+ this.context = context;
44
+ this.options = options;
45
+ this.root = createRoot(context);
46
+ this.update(context.getSnapshot(), "document");
47
+ }
48
+ update(snapshot, _kind, _change) {
49
+ this.renderSnapshot(snapshot);
50
+ }
51
+ dispose() {
52
+ this.lineView?.dispose();
53
+ this.lineView = null;
54
+ this.root.remove();
55
+ }
56
+ renderSnapshot(snapshot) {
57
+ const header = stickyScrollHeader(snapshot, this.options);
58
+ if (header.rows.length === 0) {
59
+ this.root.hidden = true;
60
+ this.layoutKey = "";
61
+ return;
62
+ }
63
+ const lineView = this.ensureLineView(snapshot);
64
+ this.syncMetrics(lineView, snapshot);
65
+ this.syncTheme(lineView, snapshot);
66
+ this.syncContent(lineView, snapshot, header);
67
+ this.syncLayout(lineView, snapshot, header);
68
+ }
69
+ ensureLineView(snapshot) {
70
+ const existing = this.lineView;
71
+ if (existing) return existing;
72
+ const created = createLineView(this.root, this.context, snapshot);
73
+ this.lineView = created;
74
+ this.appliedMetrics = snapshot.metrics;
75
+ return created;
76
+ }
77
+ syncMetrics(lineView, snapshot) {
78
+ const metrics = snapshot.metrics;
79
+ const applied = this.appliedMetrics;
80
+ if (applied?.rowHeight === metrics.rowHeight && applied.characterWidth === metrics.characterWidth) return;
81
+ this.appliedMetrics = metrics;
82
+ lineView.setTextMetrics(metrics);
83
+ }
84
+ syncTheme(lineView, snapshot) {
85
+ const theme = snapshot.theme ?? null;
86
+ if (theme === this.appliedTheme) return;
87
+ this.appliedTheme = theme;
88
+ lineView.setTheme(theme);
89
+ }
90
+ syncContent(lineView, snapshot, header) {
91
+ const key = `${snapshot.documentId}:${header.key}:${snapshot.textVersion}`;
92
+ if (key === this.contentKey && snapshot.tokens === this.appliedTokens) return;
93
+ this.contentKey = key;
94
+ this.appliedTokens = snapshot.tokens;
95
+ const content = stickyScrollContent(snapshot, header.rows);
96
+ if (content.text !== this.appliedText) {
97
+ this.appliedText = content.text;
98
+ lineView.setText(content.text);
99
+ }
100
+ lineView.setTokens(content.tokens);
101
+ }
102
+ syncLayout(lineView, snapshot, header) {
103
+ const key = [
104
+ header.top,
105
+ header.bottom,
106
+ header.stackHeight,
107
+ snapshot.viewport.scrollWidth,
108
+ snapshot.viewport.clientWidth
109
+ ].join(":");
110
+ this.root.hidden = false;
111
+ if (key === this.layoutKey) return;
112
+ this.layoutKey = key;
113
+ this.root.style.top = `${header.top}px`;
114
+ this.root.style.height = `${header.bottom - header.top}px`;
115
+ this.root.style.setProperty("--editor-sticky-scroll-content-width", `${snapshot.viewport.scrollWidth}px`);
116
+ this.root.style.setProperty("--editor-sticky-scroll-viewport-width", `${snapshot.viewport.clientWidth}px`);
117
+ lineView.setHeight(header.stackHeight);
118
+ }
119
+ };
120
+ function resolveStickyScrollOptions(options) {
121
+ return {
122
+ enabled: options.enabled ?? true,
123
+ maxLineCount: normalizeMaxLineCount(options.maxLineCount)
124
+ };
125
+ }
126
+ function normalizeMaxLineCount(value) {
127
+ if (value === void 0) return DEFAULT_MAX_LINE_COUNT;
128
+ if (!Number.isFinite(value)) return DEFAULT_MAX_LINE_COUNT;
129
+ return Math.max(1, Math.floor(value));
130
+ }
131
+ function createRoot(context) {
132
+ const root = context.scrollElement.ownerDocument.createElement("div");
133
+ root.className = "editor-sticky-scroll";
134
+ root.setAttribute("aria-hidden", "true");
135
+ root.hidden = true;
136
+ context.contentElement.appendChild(root);
137
+ return root;
138
+ }
139
+ /**
140
+ * The stack is a text view of its own rather than a second renderer, so its lines carry the tokens,
141
+ * chunking and inline replacements of the rows they mirror instead of an approximation of them.
142
+ */
143
+ function createLineView(root, context, snapshot) {
144
+ return createEditorSecondaryTextView(root, {
145
+ className: "editor-sticky-scroll-lines",
146
+ scrollMode: "static",
147
+ overscan: 0,
148
+ lineHeight: snapshot.metrics.rowHeight,
149
+ tabSize: snapshot.tabSize,
150
+ textMetrics: snapshot.metrics,
151
+ selectionHighlightName: `${context.highlightPrefix}-sticky-scroll-selection`
152
+ });
153
+ }
154
+ function stickyScrollHeader(snapshot, options) {
155
+ const geometry = stickyScrollRowGeometry(snapshot);
156
+ if (!geometry) return EMPTY_HEADER;
157
+ const rows = [];
158
+ let top = 0;
159
+ let bottom = 0;
160
+ for (const marker of stickyScrollCandidates(snapshot, geometry)) {
161
+ if (rows.at(-1) === marker.startRow) continue;
162
+ const headerRowTop = contentRowTop(geometry, marker.startRow);
163
+ const scopeBottom = contentRowBottom(geometry, marker.endRow);
164
+ const slotTop = rows.length * geometry.rowHeight;
165
+ if (slotTop <= headerRowTop - geometry.scrollTop) continue;
166
+ if (slotTop > scopeBottom - geometry.scrollTop) continue;
167
+ if (rows.length === 0) top = headerRowTop;
168
+ bottom = scopeBottom;
169
+ rows.push(marker.startRow);
170
+ if (rows.length === options.maxLineCount) break;
171
+ }
172
+ if (rows.length === 0) return EMPTY_HEADER;
173
+ return {
174
+ rows,
175
+ key: rows.join(","),
176
+ top,
177
+ bottom,
178
+ stackHeight: rows.length * geometry.rowHeight
179
+ };
180
+ }
181
+ /**
182
+ * Scopes reaching into the viewport, outermost first, for the header to slot in that order.
183
+ *
184
+ * Fold ranges never cross, so the ones still open at a given range are exactly its ancestors. A
185
+ * scope inside a collapsed one is dropped: the rows it heads are not on screen to be headed, even in
186
+ * the window where the marker set and the hidden rows disagree.
187
+ */
188
+ function stickyScrollCandidates(snapshot, geometry) {
189
+ const candidates = [];
190
+ const open = [];
191
+ for (const marker of visibleFoldMarkers(snapshot, geometry)) {
192
+ while (open.length > 0 && open.at(-1).endOffset <= marker.startOffset) open.pop();
193
+ const insideCollapsed = open.some((ancestor) => ancestor.collapsed);
194
+ open.push(marker);
195
+ if (marker.collapsed || insideCollapsed) continue;
196
+ candidates.push(marker);
197
+ }
198
+ return candidates;
199
+ }
200
+ /**
201
+ * Inclusive at both ends, unlike a scope guide's: a scope whose last row is the first mounted one is
202
+ * exactly the scope whose header the stack is there to hold.
203
+ */
204
+ function visibleFoldMarkers(snapshot, geometry) {
205
+ const markers = [];
206
+ for (const marker of snapshot.foldMarkers) {
207
+ if (marker.startRow > geometry.lastRow) continue;
208
+ if (marker.endRow < geometry.firstRow) continue;
209
+ markers.push(marker);
210
+ }
211
+ return markers.toSorted(compareMarkersByContainment);
212
+ }
213
+ /** Outermost first, which is the order a containment stack can be built in one pass. */
214
+ function compareMarkersByContainment(left, right) {
215
+ return left.startOffset - right.startOffset || right.endOffset - left.endOffset;
216
+ }
217
+ function stickyScrollRowGeometry(snapshot) {
218
+ const tops = /* @__PURE__ */ new Map();
219
+ const bottoms = /* @__PURE__ */ new Map();
220
+ let firstRow = Number.POSITIVE_INFINITY;
221
+ let lastRow = Number.NEGATIVE_INFINITY;
222
+ for (const row of snapshot.visibleRows) {
223
+ if (row.kind !== "text") continue;
224
+ const top = tops.get(row.bufferRow);
225
+ const bottom = bottoms.get(row.bufferRow);
226
+ tops.set(row.bufferRow, top === void 0 ? row.top : Math.min(top, row.top));
227
+ bottoms.set(row.bufferRow, bottom === void 0 ? row.top + row.height : Math.max(bottom, row.top + row.height));
228
+ firstRow = Math.min(firstRow, row.bufferRow);
229
+ lastRow = Math.max(lastRow, row.bufferRow);
230
+ }
231
+ if (!Number.isFinite(firstRow) || !Number.isFinite(lastRow)) return null;
232
+ return {
233
+ rowHeight: snapshot.metrics.rowHeight,
234
+ scrollTop: snapshot.viewport.scrollTop,
235
+ firstRow,
236
+ firstTop: tops.get(firstRow),
237
+ lastRow,
238
+ lastBottom: bottoms.get(lastRow),
239
+ tops,
240
+ bottoms
241
+ };
242
+ }
243
+ function contentRowTop(geometry, row) {
244
+ const top = geometry.tops.get(row);
245
+ if (top !== void 0) return top;
246
+ if (row < geometry.firstRow) return geometry.firstTop - (geometry.firstRow - row) * geometry.rowHeight;
247
+ return geometry.lastBottom + (row - geometry.lastRow - 1) * geometry.rowHeight;
248
+ }
249
+ function contentRowBottom(geometry, row) {
250
+ const bottom = geometry.bottoms.get(row);
251
+ if (bottom !== void 0) return bottom;
252
+ if (row > geometry.lastRow) return geometry.lastBottom + (row - geometry.lastRow) * geometry.rowHeight;
253
+ return geometry.firstTop - (geometry.firstRow - row - 1) * geometry.rowHeight;
254
+ }
255
+ /**
256
+ * The stack's own text, and the document's tokens rewritten into it. Both come out of one pass over
257
+ * the same row ranges, because a token offset that disagrees with the text by even one character
258
+ * paints the wrong word.
259
+ */
260
+ function stickyScrollContent(snapshot, rows) {
261
+ const textSnapshot = snapshot.textSnapshot;
262
+ const lines = [];
263
+ const tokens = [];
264
+ let base = 0;
265
+ for (const row of rows) {
266
+ const range = rowTextRange(snapshot, textSnapshot, row);
267
+ lines.push(range ? textSnapshot.readRange(range.start, range.end) : "");
268
+ if (range) appendRowTokens(tokens, snapshot.tokens, range, base);
269
+ base += (range ? range.end - range.start : 0) + 1;
270
+ }
271
+ return {
272
+ text: lines.join("\n"),
273
+ tokens
274
+ };
275
+ }
276
+ function appendRowTokens(tokens, documentTokens, range, base) {
277
+ const last = documentTokens.firstStartingAtOrAfter(range.end);
278
+ const first = documentTokens.firstEndingAfter(range.start, last);
279
+ documentTokens.forEachInRange(first, last, (start, end, styleId) => {
280
+ tokens.push({
281
+ start: base + Math.max(start, range.start) - range.start,
282
+ end: base + Math.min(end, range.end) - range.start,
283
+ style: documentTokens.styles[styleId]
284
+ });
285
+ });
286
+ }
287
+ function rowTextRange(snapshot, textSnapshot, row) {
288
+ const start = lineStartOffset(snapshot, row);
289
+ if (start === void 0) return null;
290
+ const nextStart = lineStartOffset(snapshot, row + 1) ?? textSnapshot.length + 1;
291
+ return {
292
+ start,
293
+ end: Math.max(start, Math.min(textSnapshot.length, nextStart - 1))
294
+ };
295
+ }
296
+ function lineStartOffset(snapshot, row) {
297
+ return snapshot.lineStartsView.at(row);
298
+ }
299
+ //#endregion
300
+ export { createStickyScrollPlugin };
301
+
302
+ //# sourceMappingURL=stickyScroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stickyScroll.js","names":[],"sources":["../src/stickyScroll.ts"],"sourcesContent":["import type { TextReadSnapshot } from '@singapore-editor/core/document'\nimport type { EditorTheme, VirtualizedFoldMarker } from '@singapore-editor/core/rendering'\nimport type { EditorToken, EditorTokenStore } from '@singapore-editor/core/syntax'\nimport type {\n EditorContributionChange,\n EditorPlugin,\n EditorViewContribution,\n EditorViewContributionContext,\n EditorViewContributionUpdateKind,\n EditorViewSnapshot,\n} from '@singapore-editor/core/extensions'\nimport {\n createEditorSecondaryTextView,\n type EditorSecondaryTextView,\n} from '@singapore-editor/core/secondary-views'\nimport './style.css'\n\nexport type StickyScrollPluginOptions = {\n readonly enabled?: boolean\n readonly maxLineCount?: number\n}\n\ntype ResolvedStickyScrollOptions = {\n readonly enabled: boolean\n readonly maxLineCount: number\n}\n\ntype StickyScrollHeader = {\n /** Header rows of the enclosing scopes, outermost first. */\n readonly rows: readonly number[]\n readonly key: string\n /** The headed scope in content coordinates: the top of its header row, and where it ends. */\n readonly top: number\n readonly bottom: number\n readonly stackHeight: number\n}\n\n/**\n * Where rows sit, in content coordinates, for as far as the mounted band reaches. Rows past it are\n * extrapolated from the row height, which is wrong wherever something out there is collapsed or\n * taller than a row — but every row that decides the stack is within a stack's height of the\n * viewport top, and those are mounted.\n */\ntype StickyScrollRowGeometry = {\n readonly rowHeight: number\n readonly scrollTop: number\n readonly firstRow: number\n readonly firstTop: number\n readonly lastRow: number\n readonly lastBottom: number\n readonly tops: ReadonlyMap<number, number>\n readonly bottoms: ReadonlyMap<number, number>\n}\n\ntype StickyScrollContent = {\n readonly text: string\n readonly tokens: readonly EditorToken[]\n}\n\ntype StickyScrollRowRange = {\n readonly start: number\n readonly end: number\n}\n\nconst DEFAULT_MAX_LINE_COUNT = 5\n\nconst EMPTY_HEADER: StickyScrollHeader = {\n rows: [],\n key: '',\n top: 0,\n bottom: 0,\n stackHeight: 0,\n}\n\nexport function createStickyScrollPlugin(options: StickyScrollPluginOptions = {}): EditorPlugin {\n const resolved = resolveStickyScrollOptions(options)\n\n return {\n name: 'sticky-scroll',\n activate(context) {\n return context.registerViewContribution({\n createContribution: (contributionContext) =>\n createStickyScrollContribution(contributionContext, resolved),\n })\n },\n }\n}\n\nfunction createStickyScrollContribution(\n context: EditorViewContributionContext,\n options: ResolvedStickyScrollOptions,\n): EditorViewContribution | null {\n if (!options.enabled) return null\n return new StickyScrollContribution(context, options)\n}\n\nclass StickyScrollContribution implements EditorViewContribution {\n // Which scopes the viewport hides is the whole input; the caret changes none of it.\n readonly inputs = ['content', 'tokens', 'viewport', 'layout'] as const\n private readonly context: EditorViewContributionContext\n private readonly options: ResolvedStickyScrollOptions\n private readonly root: HTMLDivElement\n private lineView: EditorSecondaryTextView | null = null\n private appliedText = ''\n private appliedTokens: EditorTokenStore | null = null\n private appliedTheme: EditorTheme | null = null\n private appliedMetrics: EditorViewSnapshot['metrics'] | null = null\n private contentKey = ''\n private layoutKey = ''\n\n public constructor(context: EditorViewContributionContext, options: ResolvedStickyScrollOptions) {\n this.context = context\n this.options = options\n this.root = createRoot(context)\n this.update(context.getSnapshot(), 'document')\n }\n\n public update(\n snapshot: EditorViewSnapshot,\n _kind: EditorViewContributionUpdateKind,\n _change?: EditorContributionChange | null,\n ): void {\n this.renderSnapshot(snapshot)\n }\n\n public dispose(): void {\n this.lineView?.dispose()\n this.lineView = null\n this.root.remove()\n }\n\n private renderSnapshot(snapshot: EditorViewSnapshot): void {\n const header = stickyScrollHeader(snapshot, this.options)\n if (header.rows.length === 0) {\n this.root.hidden = true\n this.layoutKey = ''\n return\n }\n\n const lineView = this.ensureLineView(snapshot)\n this.syncMetrics(lineView, snapshot)\n this.syncTheme(lineView, snapshot)\n this.syncContent(lineView, snapshot, header)\n this.syncLayout(lineView, snapshot, header)\n }\n\n private ensureLineView(snapshot: EditorViewSnapshot): EditorSecondaryTextView {\n const existing = this.lineView\n if (existing) return existing\n\n const created = createLineView(this.root, this.context, snapshot)\n this.lineView = created\n this.appliedMetrics = snapshot.metrics\n return created\n }\n\n // Character width too: a font change moves every column the mirrored rows draw at.\n private syncMetrics(lineView: EditorSecondaryTextView, snapshot: EditorViewSnapshot): void {\n const metrics = snapshot.metrics\n const applied = this.appliedMetrics\n if (\n applied?.rowHeight === metrics.rowHeight &&\n applied.characterWidth === metrics.characterWidth\n )\n return\n\n this.appliedMetrics = metrics\n lineView.setTextMetrics(metrics)\n }\n\n private syncTheme(lineView: EditorSecondaryTextView, snapshot: EditorViewSnapshot): void {\n const theme = snapshot.theme ?? null\n if (theme === this.appliedTheme) return\n\n this.appliedTheme = theme\n lineView.setTheme(theme)\n }\n\n private syncContent(\n lineView: EditorSecondaryTextView,\n snapshot: EditorViewSnapshot,\n header: StickyScrollHeader,\n ): void {\n // Keyed by document as well as by version, because opening another document starts its versions\n // over and the same rows there hold different text.\n const key = `${snapshot.documentId}:${header.key}:${snapshot.textVersion}`\n if (key === this.contentKey && snapshot.tokens === this.appliedTokens) return\n\n this.contentKey = key\n this.appliedTokens = snapshot.tokens\n const content = stickyScrollContent(snapshot, header.rows)\n if (content.text !== this.appliedText) {\n this.appliedText = content.text\n lineView.setText(content.text)\n }\n lineView.setTokens(content.tokens)\n }\n\n private syncLayout(\n lineView: EditorSecondaryTextView,\n snapshot: EditorViewSnapshot,\n header: StickyScrollHeader,\n ): void {\n const key = [\n header.top,\n header.bottom,\n header.stackHeight,\n snapshot.viewport.scrollWidth,\n snapshot.viewport.clientWidth,\n ].join(':')\n this.root.hidden = false\n if (key === this.layoutKey) return\n\n this.layoutKey = key\n this.root.style.top = `${header.top}px`\n this.root.style.height = `${header.bottom - header.top}px`\n this.root.style.setProperty(\n '--editor-sticky-scroll-content-width',\n `${snapshot.viewport.scrollWidth}px`,\n )\n this.root.style.setProperty(\n '--editor-sticky-scroll-viewport-width',\n `${snapshot.viewport.clientWidth}px`,\n )\n lineView.setHeight(header.stackHeight)\n }\n}\n\nfunction resolveStickyScrollOptions(\n options: StickyScrollPluginOptions,\n): ResolvedStickyScrollOptions {\n return {\n enabled: options.enabled ?? true,\n maxLineCount: normalizeMaxLineCount(options.maxLineCount),\n }\n}\n\nfunction normalizeMaxLineCount(value: number | undefined): number {\n if (value === undefined) return DEFAULT_MAX_LINE_COUNT\n if (!Number.isFinite(value)) return DEFAULT_MAX_LINE_COUNT\n return Math.max(1, Math.floor(value))\n}\n\nfunction createRoot(context: EditorViewContributionContext): HTMLDivElement {\n const root = context.scrollElement.ownerDocument.createElement('div')\n root.className = 'editor-sticky-scroll'\n root.setAttribute('aria-hidden', 'true')\n root.hidden = true\n context.contentElement.appendChild(root)\n return root\n}\n\n/**\n * The stack is a text view of its own rather than a second renderer, so its lines carry the tokens,\n * chunking and inline replacements of the rows they mirror instead of an approximation of them.\n */\nfunction createLineView(\n root: HTMLDivElement,\n context: EditorViewContributionContext,\n snapshot: EditorViewSnapshot,\n): EditorSecondaryTextView {\n return createEditorSecondaryTextView(root, {\n className: 'editor-sticky-scroll-lines',\n // Static: every line of the stack is on screen at once, so there is nothing to virtualize away.\n scrollMode: 'static',\n overscan: 0,\n lineHeight: snapshot.metrics.rowHeight,\n tabSize: snapshot.tabSize,\n // Measuring the font again could land a fraction of a pixel away from the rows being mirrored.\n textMetrics: snapshot.metrics,\n selectionHighlightName: `${context.highlightPrefix}-sticky-scroll-selection`,\n })\n}\n\nfunction stickyScrollHeader(\n snapshot: EditorViewSnapshot,\n options: ResolvedStickyScrollOptions,\n): StickyScrollHeader {\n const geometry = stickyScrollRowGeometry(snapshot)\n if (!geometry) return EMPTY_HEADER\n\n const rows: number[] = []\n let top = 0\n let bottom = 0\n for (const marker of stickyScrollCandidates(snapshot, geometry)) {\n // Scopes that open on one row — an arrow function and the object it returns — head the same row,\n // so the first of them to be accepted has already said everything the stack can say about it.\n if (rows.at(-1) === marker.startRow) continue\n\n const headerRowTop = contentRowTop(geometry, marker.startRow)\n const scopeBottom = contentRowBottom(geometry, marker.endRow)\n // The slot is the one this scope would actually be given, which is the next free row of the\n // stack: a slot counted from containment depth instead skips ahead over every enclosing scope\n // the filters below rejected, and a slot the stack never reaches admits its scope too early.\n const slotTop = rows.length * geometry.rowHeight\n // A slot above the row it would stand in for is a slot for a row still on screen, which needs no\n // stand-in; a slot below the end of its scope belongs to a scope already behind us.\n if (slotTop <= headerRowTop - geometry.scrollTop) continue\n if (slotTop > scopeBottom - geometry.scrollTop) continue\n\n if (rows.length === 0) top = headerRowTop\n // Each scope accepted after the first is nested in the one before it, so the last one accepted is\n // the first to end — and the stack travels with whichever ends first.\n bottom = scopeBottom\n rows.push(marker.startRow)\n if (rows.length === options.maxLineCount) break\n }\n\n if (rows.length === 0) return EMPTY_HEADER\n\n return { rows, key: rows.join(','), top, bottom, stackHeight: rows.length * geometry.rowHeight }\n}\n\n/**\n * Scopes reaching into the viewport, outermost first, for the header to slot in that order.\n *\n * Fold ranges never cross, so the ones still open at a given range are exactly its ancestors. A\n * scope inside a collapsed one is dropped: the rows it heads are not on screen to be headed, even in\n * the window where the marker set and the hidden rows disagree.\n */\nfunction stickyScrollCandidates(\n snapshot: EditorViewSnapshot,\n geometry: StickyScrollRowGeometry,\n): readonly VirtualizedFoldMarker[] {\n const candidates: VirtualizedFoldMarker[] = []\n const open: VirtualizedFoldMarker[] = []\n\n for (const marker of visibleFoldMarkers(snapshot, geometry)) {\n while (open.length > 0 && open.at(-1)!.endOffset <= marker.startOffset) open.pop()\n\n const insideCollapsed = open.some((ancestor) => ancestor.collapsed)\n open.push(marker)\n if (marker.collapsed || insideCollapsed) continue\n\n candidates.push(marker)\n }\n\n return candidates\n}\n\n/**\n * Inclusive at both ends, unlike a scope guide's: a scope whose last row is the first mounted one is\n * exactly the scope whose header the stack is there to hold.\n */\nfunction visibleFoldMarkers(\n snapshot: EditorViewSnapshot,\n geometry: StickyScrollRowGeometry,\n): readonly VirtualizedFoldMarker[] {\n const markers: VirtualizedFoldMarker[] = []\n for (const marker of snapshot.foldMarkers) {\n if (marker.startRow > geometry.lastRow) continue\n if (marker.endRow < geometry.firstRow) continue\n\n markers.push(marker)\n }\n\n return markers.toSorted(compareMarkersByContainment)\n}\n\n/** Outermost first, which is the order a containment stack can be built in one pass. */\nfunction compareMarkersByContainment(\n left: VirtualizedFoldMarker,\n right: VirtualizedFoldMarker,\n): number {\n return left.startOffset - right.startOffset || right.endOffset - left.endOffset\n}\n\nfunction stickyScrollRowGeometry(snapshot: EditorViewSnapshot): StickyScrollRowGeometry | null {\n const tops = new Map<number, number>()\n const bottoms = new Map<number, number>()\n let firstRow = Number.POSITIVE_INFINITY\n let lastRow = Number.NEGATIVE_INFINITY\n\n // A buffer row can hold several display rows — wrapped, or with rows injected beneath it — so its\n // box is the union of them, which is what a scope starting or ending on it is measured against.\n for (const row of snapshot.visibleRows) {\n if (row.kind !== 'text') continue\n\n const top = tops.get(row.bufferRow)\n const bottom = bottoms.get(row.bufferRow)\n tops.set(row.bufferRow, top === undefined ? row.top : Math.min(top, row.top))\n bottoms.set(\n row.bufferRow,\n bottom === undefined ? row.top + row.height : Math.max(bottom, row.top + row.height),\n )\n firstRow = Math.min(firstRow, row.bufferRow)\n lastRow = Math.max(lastRow, row.bufferRow)\n }\n\n if (!Number.isFinite(firstRow) || !Number.isFinite(lastRow)) return null\n\n return {\n rowHeight: snapshot.metrics.rowHeight,\n scrollTop: snapshot.viewport.scrollTop,\n firstRow,\n firstTop: tops.get(firstRow)!,\n lastRow,\n lastBottom: bottoms.get(lastRow)!,\n tops,\n bottoms,\n }\n}\n\nfunction contentRowTop(geometry: StickyScrollRowGeometry, row: number): number {\n const top = geometry.tops.get(row)\n if (top !== undefined) return top\n if (row < geometry.firstRow) {\n return geometry.firstTop - (geometry.firstRow - row) * geometry.rowHeight\n }\n\n return geometry.lastBottom + (row - geometry.lastRow - 1) * geometry.rowHeight\n}\n\nfunction contentRowBottom(geometry: StickyScrollRowGeometry, row: number): number {\n const bottom = geometry.bottoms.get(row)\n if (bottom !== undefined) return bottom\n if (row > geometry.lastRow) {\n return geometry.lastBottom + (row - geometry.lastRow) * geometry.rowHeight\n }\n\n return geometry.firstTop - (geometry.firstRow - row - 1) * geometry.rowHeight\n}\n\n/**\n * The stack's own text, and the document's tokens rewritten into it. Both come out of one pass over\n * the same row ranges, because a token offset that disagrees with the text by even one character\n * paints the wrong word.\n */\nfunction stickyScrollContent(\n snapshot: EditorViewSnapshot,\n rows: readonly number[],\n): StickyScrollContent {\n const textSnapshot = snapshot.textSnapshot\n const lines: string[] = []\n const tokens: EditorToken[] = []\n let base = 0\n\n for (const row of rows) {\n const range = rowTextRange(snapshot, textSnapshot, row)\n lines.push(range ? textSnapshot.readRange(range.start, range.end) : '')\n if (range) appendRowTokens(tokens, snapshot.tokens, range, base)\n base += (range ? range.end - range.start : 0) + 1\n }\n\n return { text: lines.join('\\n'), tokens }\n}\n\nfunction appendRowTokens(\n tokens: EditorToken[],\n documentTokens: EditorTokenStore,\n range: StickyScrollRowRange,\n base: number,\n): void {\n const last = documentTokens.firstStartingAtOrAfter(range.end)\n const first = documentTokens.firstEndingAfter(range.start, last)\n documentTokens.forEachInRange(first, last, (start, end, styleId) => {\n tokens.push({\n start: base + Math.max(start, range.start) - range.start,\n end: base + Math.min(end, range.end) - range.start,\n style: documentTokens.styles[styleId]!,\n })\n })\n}\n\nfunction rowTextRange(\n snapshot: EditorViewSnapshot,\n textSnapshot: TextReadSnapshot,\n row: number,\n): StickyScrollRowRange | null {\n const start = lineStartOffset(snapshot, row)\n if (start === undefined) return null\n\n const nextStart = lineStartOffset(snapshot, row + 1) ?? textSnapshot.length + 1\n return { start, end: Math.max(start, Math.min(textSnapshot.length, nextStart - 1)) }\n}\n\nfunction lineStartOffset(snapshot: EditorViewSnapshot, row: number): number | undefined {\n return snapshot.lineStartsView.at(row)\n}\n"],"mappings":"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"}
package/dist/style.css ADDED
@@ -0,0 +1,153 @@
1
+ .editor-scope-lines {
2
+ --editor-scope-line-color: rgba(161, 161, 170, 0.26);
3
+ --editor-scope-line-active-color: rgba(212, 212, 216, 0.52);
4
+ --editor-scope-line-color-0: color-mix(in srgb, var(--editor-syntax-type) 34%, transparent);
5
+ --editor-scope-line-color-1: color-mix(in srgb, var(--editor-syntax-keyword) 30%, transparent);
6
+ --editor-scope-line-color-2: color-mix(in srgb, var(--editor-syntax-string) 28%, transparent);
7
+ --editor-scope-line-color-3: color-mix(in srgb, var(--editor-syntax-number) 32%, transparent);
8
+ --editor-scope-line-color-4: color-mix(in srgb, var(--editor-syntax-function) 30%, transparent);
9
+ --editor-scope-line-color-5: color-mix(
10
+ in srgb,
11
+ var(--editor-syntax-variable-builtin) 30%,
12
+ transparent
13
+ );
14
+ --editor-scope-line-active-color-0: color-mix(
15
+ in srgb,
16
+ var(--editor-syntax-type) 58%,
17
+ transparent
18
+ );
19
+ --editor-scope-line-active-color-1: color-mix(
20
+ in srgb,
21
+ var(--editor-syntax-keyword) 54%,
22
+ transparent
23
+ );
24
+ --editor-scope-line-active-color-2: color-mix(
25
+ in srgb,
26
+ var(--editor-syntax-string) 50%,
27
+ transparent
28
+ );
29
+ --editor-scope-line-active-color-3: color-mix(
30
+ in srgb,
31
+ var(--editor-syntax-number) 56%,
32
+ transparent
33
+ );
34
+ --editor-scope-line-active-color-4: color-mix(
35
+ in srgb,
36
+ var(--editor-syntax-function) 54%,
37
+ transparent
38
+ );
39
+ --editor-scope-line-active-color-5: color-mix(
40
+ in srgb,
41
+ var(--editor-syntax-variable-builtin) 54%,
42
+ transparent
43
+ );
44
+ --editor-scope-line-width: 1px;
45
+
46
+ height: 0;
47
+ left: var(--editor-gutter-width, 0px);
48
+ min-width: max(100%, var(--editor-scope-lines-content-width, 0px));
49
+ pointer-events: none;
50
+ position: absolute;
51
+ top: 0;
52
+ /* Own compositor layer: WebKit re-resolves all CSS Highlight ranges when
53
+ children of an in-flow layer churn; see .editor-virtualized-row. */
54
+ will-change: transform;
55
+ z-index: 1;
56
+ }
57
+
58
+ .editor-scope-line {
59
+ background: var(--editor-scope-line-color);
60
+ position: absolute;
61
+ width: var(--editor-scope-line-width);
62
+ }
63
+
64
+ .editor-virtualized[data-editor-presentation='provisional'] .editor-scope-lines {
65
+ visibility: hidden;
66
+ }
67
+
68
+ .editor-scope-line[data-editor-scope-line-level='0'] {
69
+ background: var(--editor-scope-line-color-0);
70
+ }
71
+
72
+ .editor-scope-line[data-editor-scope-line-level='1'] {
73
+ background: var(--editor-scope-line-color-1);
74
+ }
75
+
76
+ .editor-scope-line[data-editor-scope-line-level='2'] {
77
+ background: var(--editor-scope-line-color-2);
78
+ }
79
+
80
+ .editor-scope-line[data-editor-scope-line-level='3'] {
81
+ background: var(--editor-scope-line-color-3);
82
+ }
83
+
84
+ .editor-scope-line[data-editor-scope-line-level='4'] {
85
+ background: var(--editor-scope-line-color-4);
86
+ }
87
+
88
+ .editor-scope-line[data-editor-scope-line-level='5'] {
89
+ background: var(--editor-scope-line-color-5);
90
+ }
91
+
92
+ .editor-scope-line-active {
93
+ background: var(--editor-scope-line-active-color);
94
+ }
95
+
96
+ .editor-scope-line-active[data-editor-scope-line-level='0'] {
97
+ background: var(--editor-scope-line-active-color-0);
98
+ }
99
+
100
+ .editor-scope-line-active[data-editor-scope-line-level='1'] {
101
+ background: var(--editor-scope-line-active-color-1);
102
+ }
103
+
104
+ .editor-scope-line-active[data-editor-scope-line-level='2'] {
105
+ background: var(--editor-scope-line-active-color-2);
106
+ }
107
+
108
+ .editor-scope-line-active[data-editor-scope-line-level='3'] {
109
+ background: var(--editor-scope-line-active-color-3);
110
+ }
111
+
112
+ .editor-scope-line-active[data-editor-scope-line-level='4'] {
113
+ background: var(--editor-scope-line-active-color-4);
114
+ }
115
+
116
+ .editor-scope-line-active[data-editor-scope-line-level='5'] {
117
+ background: var(--editor-scope-line-active-color-5);
118
+ }
119
+
120
+ /* The layer is a box over the scope the stack heads, in content coordinates, and is JS-positioned
121
+ on the rows of that scope. It exists to be the stack's containing block: a sticky box is held
122
+ inside the one it belongs to, so the stack is pinned to the top of the viewport while this box is
123
+ on screen and is slid up and out of it, pixel by pixel, as the box's end passes — which is the
124
+ push-out, computed by the browser on every frame instead of on every notification. */
125
+ .editor-sticky-scroll {
126
+ /* Resolve the parent gutter here before the secondary text view shadows it. */
127
+ --editor-sticky-scroll-inset: var(--editor-gutter-width, 0px);
128
+
129
+ left: 0;
130
+ min-width: max(100%, var(--editor-sticky-scroll-content-width, 0px));
131
+ pointer-events: none;
132
+ position: absolute;
133
+ z-index: var(--editor-z-overlay-surface);
134
+ }
135
+
136
+ /* Compound so that it outranks the text view's own rules whichever order the two stylesheets happen
137
+ to load in, rather than only when this one loads second. */
138
+ .editor-virtualized.editor-sticky-scroll-lines {
139
+ --editor-sticky-scroll-shadow: rgba(0, 0, 0, 0.42);
140
+
141
+ /* The stack stays clear of the gutter while its parent handles minimap clipping. */
142
+ box-shadow: 0 6px 6px -6px var(--editor-sticky-scroll-shadow);
143
+ left: var(--editor-sticky-scroll-inset, 0px);
144
+ /* Nothing about the stack is scrollable: its height is exactly the lines it holds, and a scrollbar
145
+ of its own would move it out of step with the rows it mirrors. */
146
+ overflow: hidden;
147
+ pointer-events: none;
148
+ position: sticky;
149
+ top: 0;
150
+ width: calc(
151
+ var(--editor-sticky-scroll-viewport-width, 0px) - var(--editor-sticky-scroll-inset, 0px)
152
+ );
153
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@singapore-editor/scope-lines",
3
+ "version": "0.1.1",
4
+ "files": [
5
+ "README.md",
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "sideEffects": [
10
+ "./src/style.css",
11
+ "./dist/style.css"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./style.css": "./dist/style.css"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "scripts": {
26
+ "build": "bun ../../scripts/build-package.ts",
27
+ "test": "vitest run",
28
+ "typecheck": "tsgo --noEmit",
29
+ "lint": "oxlint .",
30
+ "format": "oxfmt --write .",
31
+ "format:check": "oxfmt --check ."
32
+ },
33
+ "dependencies": {
34
+ "@singapore-editor/core": "0.1.2"
35
+ },
36
+ "devDependencies": {
37
+ "@playwright/test": "^1.63.0",
38
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
39
+ "@vitest/browser-playwright": "^5.0.2",
40
+ "happy-dom": "^20.14.5",
41
+ "oxfmt": "0.70.0",
42
+ "oxlint": "1.85.0",
43
+ "typescript": "~6.0.3",
44
+ "vitest": "^5.0.2"
45
+ }
46
+ }