@bhsd/codemirror-stickyscroll 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/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/compute.d.ts +7 -0
- package/dist/compute.js +168 -0
- package/dist/facet.d.ts +47 -0
- package/dist/facet.js +81 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +24 -0
- package/dist/plugin.d.ts +62 -0
- package/dist/plugin.js +333 -0
- package/dist/render.d.ts +22 -0
- package/dist/render.js +140 -0
- package/dist/theme.d.ts +19 -0
- package/dist/theme.js +73 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zulfazli (Fazelllyyy)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @bhsd/codemirror-stickyscroll
|
|
2
|
+
|
|
3
|
+
> VS Code / Monaco-style **sticky scroll** (sticky lines) for [CodeMirror 6](https://codemirror.net/).
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="preview/image.png" alt="Sticky Scroll Preview" width="750" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
This is a fork of [@fazelstudio/codemirror-stickyscroll](https://github.com/fazel-studio/codemirror-stickyscroll).
|
|
10
|
+
|
|
11
|
+
Sticky lines keep the *opening* lines of the enclosing scopes (function, class,
|
|
12
|
+
if/loop blocks, …) pinned at the top of the editor while you scroll — exactly
|
|
13
|
+
like VS Code's sticky scroll, but built **purely as an external CodeMirror 6
|
|
14
|
+
extension** (no fork of `@codemirror/*`).
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Per-pixel updates** — a native `scroll` listener + `requestAnimationFrame`
|
|
19
|
+
throttle keeps the bar glued to the scroll position; no "jumpy" updates.
|
|
20
|
+
- **Click-to-jump with margin compensation** — clicking a sticky line scrolls
|
|
21
|
+
the target line to the top *plus* the current bar height, so the line you
|
|
22
|
+
jump to is never hidden behind the bar. Keyboard (`Enter`/`Space`) + `role="button"` for a11y.
|
|
23
|
+
- **Reuses the consumer's theme** — the bar re-highlights lines through the
|
|
24
|
+
*active* highlight styles of the editor (`highlightingFor`), or clones the
|
|
25
|
+
already-rendered DOM line when available. The package **never** registers its
|
|
26
|
+
own `syntaxHighlighting(...)`.
|
|
27
|
+
- **Language-agnostic** — detection is based on `foldable()` from
|
|
28
|
+
`@codemirror/language` (the fold services / fold node props that every
|
|
29
|
+
`@codemirror/lang-*` already registers). It includes a smart, generic denylist
|
|
30
|
+
that works across multiple languages (JS/TS, Python, Rust, Go, etc.) out of the box,
|
|
31
|
+
and gracefully handles data languages like JSON.
|
|
32
|
+
- **Gutter alignment, horizontal sync, RTL, resize-proof** — line numbers are
|
|
33
|
+
aligned with the real gutter (width tracked via `ResizeObserver`), the bar
|
|
34
|
+
follows horizontal scroll, and it reacts to font/zoom/resize changes.
|
|
35
|
+
- **Zero runtime deps** — peer dependencies only.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @bhsd/codemirror-stickyscroll
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The following are **peer dependencies** (already present in any project that
|
|
44
|
+
has a working CodeMirror 6 editor):
|
|
45
|
+
|
|
46
|
+
| Package | Minimum |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `@codemirror/view` | ^6.0.0 |
|
|
49
|
+
| `@codemirror/state` | ^6.0.0 |
|
|
50
|
+
| `@codemirror/language` | ^6.0.0 |
|
|
51
|
+
| `@lezer/common` | ^1.0.0 |
|
|
52
|
+
| `@lezer/highlight` | ^1.0.0 |
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
### `stickyScroll(options?: StickyScrollOptions): Extension`
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
interface StickyScrollOptions {
|
|
60
|
+
/** Maximum sticky lines shown at once (dynamically clamped to ~40% of editor height). Default: 4 */
|
|
61
|
+
maxStickyLines?: number;
|
|
62
|
+
/** Minimum lines a scope must span to become sticky. Default: 2 */
|
|
63
|
+
minBlockLines?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Denylist predicate: return true to never pin a foldable node of that type.
|
|
66
|
+
* Defaults to `defaultExcludeNode` (imports + data literals + comments).
|
|
67
|
+
* It uses exact node names for JS/TS and generic regex patterns for other
|
|
68
|
+
* languages, while automatically bypassing the literal denylist for JSON.
|
|
69
|
+
*/
|
|
70
|
+
excludeNode?: (nodeName: string, langName: string | undefined) => boolean;
|
|
71
|
+
/** Extra HighlightStyle merged in when a line is re-highlighted from scratch. */
|
|
72
|
+
highlightStyle?: HighlightStyle;
|
|
73
|
+
/** Called after a sticky line is clicked (after the jump is dispatched). */
|
|
74
|
+
onLineClick?: (lineNumber: number) => void;
|
|
75
|
+
/** Extra CSS class(es) for the bar container. */
|
|
76
|
+
class?: string;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Re-exports
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import {
|
|
84
|
+
stickyScroll,
|
|
85
|
+
stickyScrollFacet, // the configuration Facet (compose/override per instance)
|
|
86
|
+
defaultExcludeNode, // default denylist implementation
|
|
87
|
+
makeStickyScrollConfig, // merge options with defaults
|
|
88
|
+
stickyScrollBaseTheme, // layout-only base theme (no token colors)
|
|
89
|
+
type StickyScrollOptions,
|
|
90
|
+
type StickyLine,
|
|
91
|
+
} from "@bhsd/codemirror-stickyscroll";
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Styling
|
|
95
|
+
|
|
96
|
+
The bar is layout-only by default and always follows the **editor's own theme**
|
|
97
|
+
(token colors, background). You can restyle it with CSS.
|
|
98
|
+
|
|
99
|
+
Per-row hooks:
|
|
100
|
+
|
|
101
|
+
| Class | Purpose |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `.cm-stickyscroll-container` | the overlay bar |
|
|
104
|
+
| `.cm-stickyscroll-line` | one breadcrumb row |
|
|
105
|
+
| `.cm-stickyscroll-line:hover` | hover highlight |
|
|
106
|
+
| `.cm-stickyscroll-line.cm-stickyscroll-current` | block containing the cursor |
|
|
107
|
+
| `.cm-stickyscroll-gutter` | line-number cell |
|
|
108
|
+
| `.cm-stickyscroll-text` | code column (horizontally synced) |
|
|
109
|
+
|
|
110
|
+
## How it works
|
|
111
|
+
|
|
112
|
+
1. **Detect the top line** — `view.lineBlockAtHeight(scrollTop - documentTop)`.
|
|
113
|
+
2. **Walk the syntax tree** — from `syntaxTree(state).resolveInner(topPos, 0)`
|
|
114
|
+
up through `node.parent`.
|
|
115
|
+
3. **A scope qualifies when** its opening line is *foldable*
|
|
116
|
+
(`foldable(state, line.from, line.to)` from `@codemirror/language`), its fold
|
|
117
|
+
anchor node is not denylisted, and it spans ≥ `minBlockLines`.
|
|
118
|
+
4. **Render** — a `ViewPlugin` owns an absolutely-positioned overlay
|
|
119
|
+
(`position: absolute; top: 0` inside `view.dom`), updated per-frame from the
|
|
120
|
+
native `scroll` event; line numbers align with the real gutter; token colors
|
|
121
|
+
come from the consumer's active highlighters.
|
|
122
|
+
|
|
123
|
+
The DOM overlay (rather than the Panel API / `showPanel`) is what makes the bar
|
|
124
|
+
behave like Monaco: it never pushes the content down (no reflow) and it can be
|
|
125
|
+
updated per-pixel, which panels cannot (panels only re-render on `ViewUpdate`,
|
|
126
|
+
not on pure scroll).
|
|
127
|
+
|
|
128
|
+
## Language support
|
|
129
|
+
|
|
130
|
+
Works out of the box with any `@codemirror/lang-*` (or community grammar) that
|
|
131
|
+
registers folding — JS/TS, Rust, Python, Java, Go, C#, etc.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT — © Zulfazli (Fazelllyyy) · [https://github.com/fazelllyyy](https://github.com/fazelllyyy)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EditorView } from "@codemirror/view";
|
|
2
|
+
import type { StickyScrollConfig } from "./facet";
|
|
3
|
+
import type { StickyLine } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Compute the sticky context for the current scroll position (viewport top).
|
|
6
|
+
*/
|
|
7
|
+
export declare function getStickyContext(view: EditorView, config: StickyScrollConfig): StickyLine[];
|
package/dist/compute.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ensureSyntaxTree, foldNodeProp, foldService, language, syntaxTree } from "@codemirror/language";
|
|
2
|
+
/**
|
|
3
|
+
* Milliseconds of synchronous parse work we are allowed to spend per sticky
|
|
4
|
+
* update when the committed syntax tree has not caught up with the viewport
|
|
5
|
+
* yet (large files / big scroll jumps). The background parser keeps working
|
|
6
|
+
* between frames and commits its results, so this only accelerates the first
|
|
7
|
+
* reveal; once the tree covers the position the cost is O(1).
|
|
8
|
+
*/
|
|
9
|
+
const PARSE_TIMEOUT = 25;
|
|
10
|
+
/**
|
|
11
|
+
* Return the best syntax tree we can answer a query at `pos` with right now.
|
|
12
|
+
*
|
|
13
|
+
* When the committed tree already covers `pos`, it is used directly (cheap).
|
|
14
|
+
* Otherwise we synchronously advance the parser for a bounded amount of time
|
|
15
|
+
* and use the extended tree if it reached `pos`, falling back to the committed
|
|
16
|
+
* tree otherwise (the background parser commits the rest shortly after).
|
|
17
|
+
*
|
|
18
|
+
* This is what keeps the sticky bar correct on files with thousands of lines:
|
|
19
|
+
* without it, `syntaxTree(state)` still holds a stale, short parse right after
|
|
20
|
+
* a scroll jump, and the ancestor walk would clamp to the document root and
|
|
21
|
+
* return no sticky lines at all.
|
|
22
|
+
*/
|
|
23
|
+
function treeUpTo(state, pos) {
|
|
24
|
+
var _a;
|
|
25
|
+
const committed = syntaxTree(state);
|
|
26
|
+
if (committed.length >= pos)
|
|
27
|
+
return committed;
|
|
28
|
+
return (_a = ensureSyntaxTree(state, pos, PARSE_TIMEOUT)) !== null && _a !== void 0 ? _a : committed;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compute the sticky context for the current scroll position (viewport top).
|
|
32
|
+
*/
|
|
33
|
+
export function getStickyContext(view, config) {
|
|
34
|
+
const state = view.state;
|
|
35
|
+
const lang = state.facet(language);
|
|
36
|
+
if (!lang)
|
|
37
|
+
return [];
|
|
38
|
+
const tree = syntaxTree(state);
|
|
39
|
+
if (tree.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
// Calculate the document-relative Y of the top edge of the visible viewport.
|
|
42
|
+
// `view.documentTop` is the client Y of the document's top edge.
|
|
43
|
+
// The viewport's top client Y is `scrollDOM.getBoundingClientRect().top`.
|
|
44
|
+
const clientTop = view.scrollDOM.getBoundingClientRect().top;
|
|
45
|
+
let y = clientTop - view.documentTop;
|
|
46
|
+
if (y < 0)
|
|
47
|
+
y = 0;
|
|
48
|
+
const topBlock = view.lineBlockAtHeight(y);
|
|
49
|
+
if (topBlock.from > state.doc.length)
|
|
50
|
+
return [];
|
|
51
|
+
// Make sure the parser has covered at least the whole rendered range before
|
|
52
|
+
// walking ancestors, so deeply-scrolled positions in big files resolve to
|
|
53
|
+
// real scopes instead of the (clamped) document root.
|
|
54
|
+
const upto = Math.min(view.viewport.to, state.doc.length);
|
|
55
|
+
const parseTree = treeUpTo(state, upto);
|
|
56
|
+
// Dynamic clamp: limit sticky bar to max ~40% of the editor height
|
|
57
|
+
const lineHeight = Math.round(view.defaultLineHeight) || 16;
|
|
58
|
+
const maxDynamic = Math.max(1, Math.floor(view.scrollDOM.clientHeight * 0.4 / lineHeight));
|
|
59
|
+
const maxSticky = Math.min(config.maxStickyLines, maxDynamic);
|
|
60
|
+
return getStickyContextForRange(view, topBlock.from, config, parseTree, maxSticky);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pure version of the algorithm.
|
|
64
|
+
*
|
|
65
|
+
* `tree` is optional and defaults to the committed syntax tree; pass an
|
|
66
|
+
* already-extended tree (see `getStickyContext`) to compute against a parse
|
|
67
|
+
* that is known to cover `fromPos`.
|
|
68
|
+
*/
|
|
69
|
+
function getStickyContextForRange({ state, viewport }, fromPos, config, t, maxSticky) {
|
|
70
|
+
const lang = state.facet(language);
|
|
71
|
+
if (!lang)
|
|
72
|
+
return [];
|
|
73
|
+
if (t.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
const doc = state.doc;
|
|
76
|
+
if (fromPos > doc.length)
|
|
77
|
+
return [];
|
|
78
|
+
const topLineNumber = doc.lineAt(fromPos).number;
|
|
79
|
+
const minBlockLines = config.minBlockLines;
|
|
80
|
+
const exclude = config.excludeNode;
|
|
81
|
+
const langName = lang.name;
|
|
82
|
+
const found = [];
|
|
83
|
+
const services = state.facet(foldService);
|
|
84
|
+
if (services.length === 0) {
|
|
85
|
+
let node = t.resolveInner(fromPos, 0);
|
|
86
|
+
while (node) {
|
|
87
|
+
if (!node.type.isTop && node.from < node.to && node.to <= doc.length) {
|
|
88
|
+
const prop = node.type.prop(foldNodeProp);
|
|
89
|
+
if (prop) {
|
|
90
|
+
const foldRange = prop(node, state);
|
|
91
|
+
if (foldRange) {
|
|
92
|
+
// Find the semantic owner of this foldable block
|
|
93
|
+
let owner = node;
|
|
94
|
+
if (node.parent && !node.parent.type.isTop && !node.parent.type.prop(foldNodeProp)) {
|
|
95
|
+
owner = node.parent;
|
|
96
|
+
}
|
|
97
|
+
const open = doc.lineAt(owner.from);
|
|
98
|
+
const openLine = open.number;
|
|
99
|
+
// Opening line must already be scrolled out above the viewport, and must not be blank.
|
|
100
|
+
if (openLine < topLineNumber && open.text.trim() !== "") {
|
|
101
|
+
// The denylist describes *foldable* node types (data literals,
|
|
102
|
+
// comments, imports). `owner` may be a wrapper (e.g.
|
|
103
|
+
// VariableDeclaration wrapping an ObjectExpression), so evaluate
|
|
104
|
+
// against the foldable node's own type name.
|
|
105
|
+
const typeName = node.name;
|
|
106
|
+
if (!exclude(typeName, langName, owner.name)) {
|
|
107
|
+
// A scope only counts as "over" once its full semantic end has
|
|
108
|
+
// passed, so measure from `owner.to` (e.g. the whole try/catch or
|
|
109
|
+
// if/else statement) instead of just the foldable block. This is
|
|
110
|
+
// what delays the slide-away until the real closing brace.
|
|
111
|
+
const close = doc.lineAt(Math.min(owner.to, doc.length));
|
|
112
|
+
const closeLine = close.number;
|
|
113
|
+
if (closeLine - openLine + 1 >= minBlockLines && closeLine >= topLineNumber) {
|
|
114
|
+
found.push({
|
|
115
|
+
lineNumber: openLine,
|
|
116
|
+
from: open.from,
|
|
117
|
+
to: open.to,
|
|
118
|
+
text: open.text,
|
|
119
|
+
nodeFrom: owner.from,
|
|
120
|
+
nodeTo: owner.to,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
node = node.parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
for (let openLine = topLineNumber - 1; openLine > 0; openLine--) {
|
|
133
|
+
const open = doc.line(openLine);
|
|
134
|
+
if (open.to < viewport.from)
|
|
135
|
+
break;
|
|
136
|
+
const ownerName = open.text.trim();
|
|
137
|
+
if (!exclude(undefined, langName, ownerName)) {
|
|
138
|
+
for (const service of services) {
|
|
139
|
+
const foldRange = service(state, open.from, open.to);
|
|
140
|
+
if (foldRange) {
|
|
141
|
+
const nodeTo = Math.min(foldRange.to, doc.length);
|
|
142
|
+
const close = doc.lineAt(nodeTo);
|
|
143
|
+
const closeLine = close.number;
|
|
144
|
+
if (closeLine - openLine + 1 >= minBlockLines && closeLine >= topLineNumber) {
|
|
145
|
+
found.push({
|
|
146
|
+
lineNumber: openLine,
|
|
147
|
+
from: open.from,
|
|
148
|
+
to: open.to,
|
|
149
|
+
text: open.text,
|
|
150
|
+
nodeFrom: foldRange.from,
|
|
151
|
+
nodeTo,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// `found` is collected innermost → outermost. Reverse to outermost → innermost,
|
|
160
|
+
// dedup by opening line (keep the outermost node on that line), then cap.
|
|
161
|
+
found.reverse();
|
|
162
|
+
const dedup = new Map();
|
|
163
|
+
for (const line of found) {
|
|
164
|
+
if (!dedup.has(line.lineNumber))
|
|
165
|
+
dedup.set(line.lineNumber, line);
|
|
166
|
+
}
|
|
167
|
+
return Array.from(dedup.values()).slice(0, maxSticky);
|
|
168
|
+
}
|
package/dist/facet.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Facet } from "@codemirror/state";
|
|
2
|
+
/**
|
|
3
|
+
* Public options accepted by `stickyScroll()`.
|
|
4
|
+
*
|
|
5
|
+
* All fields are optional; defaults are documented below.
|
|
6
|
+
*/
|
|
7
|
+
export interface StickyScrollOptions {
|
|
8
|
+
/** Maximum sticky lines shown at once. Default: 4 */
|
|
9
|
+
maxStickyLines?: number;
|
|
10
|
+
/** Minimum number of lines a scope must span before it becomes sticky. Default: 6 */
|
|
11
|
+
minBlockLines?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Denylist predicate. When it returns `true` for a foldable syntax node,
|
|
14
|
+
* that node is never turned into a sticky line, although it could be folded.
|
|
15
|
+
* Defaults to `defaultExcludeNode`, which excludes imports and data
|
|
16
|
+
* literals — exact node names for JS/TS, best-effort name-pattern
|
|
17
|
+
* matching for other grammars. Override for fine-tuning, especially for
|
|
18
|
+
* a specific non-JS/TS language.
|
|
19
|
+
*/
|
|
20
|
+
excludeNode?: (nodeName?: string, langName?: string, ownerName?: string) => boolean;
|
|
21
|
+
/** Extra CSS class(es) applied to the sticky bar container. */
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Resolved configuration produced by `stickyScrollFacet`. */
|
|
25
|
+
export interface StickyScrollConfig extends Required<Pick<StickyScrollOptions, "maxStickyLines" | "minBlockLines" | "excludeNode">> {
|
|
26
|
+
class?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Default denylist (see §4.3 of the design doc): nodes that *are* foldable but
|
|
30
|
+
* semantically are not navigation scopes worth pinning (imports, big data
|
|
31
|
+
* literals, comment blocks).
|
|
32
|
+
*
|
|
33
|
+
* Precise for JS/TS out of the box (`JS_TS_DENYLIST`); best-effort pattern
|
|
34
|
+
* match for everything else. Override via `StickyScrollOptions.excludeNode`
|
|
35
|
+
* for a language you need exact behavior on.
|
|
36
|
+
*/
|
|
37
|
+
export declare const defaultExcludeNode: (nodeName?: string, langName?: string, ownerName?: string) => boolean;
|
|
38
|
+
/** Merge partial options with built-in defaults. */
|
|
39
|
+
export declare function makeStickyScrollConfig(options?: StickyScrollOptions): StickyScrollConfig;
|
|
40
|
+
/**
|
|
41
|
+
* The facet carrying the sticky-scroll configuration.
|
|
42
|
+
*
|
|
43
|
+
* Multiple `stickyScroll()` instances compose; the *last* registered value wins
|
|
44
|
+
* (override semantics). Consumers may always read/override it per-instance via
|
|
45
|
+
* `stickyScrollFacet.of(...)`.
|
|
46
|
+
*/
|
|
47
|
+
export declare const stickyScrollFacet: Facet<StickyScrollOptions, StickyScrollConfig>;
|
package/dist/facet.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Facet } from "@codemirror/state";
|
|
2
|
+
/**
|
|
3
|
+
* Exact node names we know for certain, verified against the JS/TS Lezer
|
|
4
|
+
* grammar (`@codemirror/lang-javascript`). Kept as an explicit set (rather
|
|
5
|
+
* than folded into the pattern fallback below) because these are the names
|
|
6
|
+
* we can vouch for precisely.
|
|
7
|
+
*/
|
|
8
|
+
const JS_TS_DENYLIST = new Set([
|
|
9
|
+
"ImportDeclaration",
|
|
10
|
+
"ImportStatement",
|
|
11
|
+
"ObjectExpression",
|
|
12
|
+
"ObjectLiteral",
|
|
13
|
+
"ArrayExpression",
|
|
14
|
+
"ArrayLiteral",
|
|
15
|
+
"ObjectPattern",
|
|
16
|
+
"Comment",
|
|
17
|
+
"LineComment",
|
|
18
|
+
"BlockComment",
|
|
19
|
+
"DocComment",
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* Best-effort fallback for grammars we don't special-case above (Python,
|
|
23
|
+
* Rust, Go, CSS, ...). Every Lezer grammar names its nodes independently, so
|
|
24
|
+
* there is no single exact list that covers all of them — but most `lang-*`
|
|
25
|
+
* packages follow a similar naming convention for the three categories that
|
|
26
|
+
* matter here (data literals, comments, import/use declarations). These
|
|
27
|
+
* patterns catch that convention on a best-effort basis; they will not be
|
|
28
|
+
* 100% precise for every language, but they degrade gracefully (a miss just
|
|
29
|
+
* means the node isn't excluded, not a crash or a wrong result elsewhere).
|
|
30
|
+
* For languages where precision matters, pass a language-specific
|
|
31
|
+
* `excludeNode` via `StickyScrollOptions`.
|
|
32
|
+
*/
|
|
33
|
+
const LITERAL_NODE_PATTERN = /^(?:Object|Array|List|Dict(?:ionary)?|Set|Record|Map|Tuple)(?:Expression|Literal|Pattern)?$/;
|
|
34
|
+
const IMPORT_NODE_PATTERN = /^(?:Import|Use)(?:Declaration|Statement|Item|Spec)?$/;
|
|
35
|
+
/**
|
|
36
|
+
* Default denylist (see §4.3 of the design doc): nodes that *are* foldable but
|
|
37
|
+
* semantically are not navigation scopes worth pinning (imports, big data
|
|
38
|
+
* literals, comment blocks).
|
|
39
|
+
*
|
|
40
|
+
* Precise for JS/TS out of the box (`JS_TS_DENYLIST`); best-effort pattern
|
|
41
|
+
* match for everything else. Override via `StickyScrollOptions.excludeNode`
|
|
42
|
+
* for a language you need exact behavior on.
|
|
43
|
+
*/
|
|
44
|
+
export const defaultExcludeNode = (nodeName, langName, ownerName) => {
|
|
45
|
+
// In pure data languages like JSON, we DO NOT exclude objects/arrays.
|
|
46
|
+
// Because the entire JSON file consists of Objects/Arrays, excluding them
|
|
47
|
+
// would completely disable sticky scroll for the file.
|
|
48
|
+
if (langName === "json" || langName === "jsonc") {
|
|
49
|
+
return ownerName !== "Property" || nodeName.endsWith("Comment");
|
|
50
|
+
}
|
|
51
|
+
// DO NOT exclude in stream-base languages.
|
|
52
|
+
if (nodeName === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
return (JS_TS_DENYLIST.has(nodeName) ||
|
|
55
|
+
nodeName.endsWith("Comment") ||
|
|
56
|
+
LITERAL_NODE_PATTERN.test(nodeName) ||
|
|
57
|
+
IMPORT_NODE_PATTERN.test(nodeName));
|
|
58
|
+
};
|
|
59
|
+
const DEFAULT_MAX_STICKY_LINES = 4;
|
|
60
|
+
const DEFAULT_MIN_BLOCK_LINES = 6;
|
|
61
|
+
/** Merge partial options with built-in defaults. */
|
|
62
|
+
export function makeStickyScrollConfig(options) {
|
|
63
|
+
var _a, _b, _c;
|
|
64
|
+
return {
|
|
65
|
+
maxStickyLines: (_a = options === null || options === void 0 ? void 0 : options.maxStickyLines) !== null && _a !== void 0 ? _a : DEFAULT_MAX_STICKY_LINES,
|
|
66
|
+
minBlockLines: (_b = options === null || options === void 0 ? void 0 : options.minBlockLines) !== null && _b !== void 0 ? _b : DEFAULT_MIN_BLOCK_LINES,
|
|
67
|
+
excludeNode: (_c = options === null || options === void 0 ? void 0 : options.excludeNode) !== null && _c !== void 0 ? _c : defaultExcludeNode,
|
|
68
|
+
class: options === null || options === void 0 ? void 0 : options.class,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The facet carrying the sticky-scroll configuration.
|
|
73
|
+
*
|
|
74
|
+
* Multiple `stickyScroll()` instances compose; the *last* registered value wins
|
|
75
|
+
* (override semantics). Consumers may always read/override it per-instance via
|
|
76
|
+
* `stickyScrollFacet.of(...)`.
|
|
77
|
+
*/
|
|
78
|
+
export const stickyScrollFacet = Facet.define({
|
|
79
|
+
compare: (a, b) => a === b,
|
|
80
|
+
combine: (values) => makeStickyScrollConfig(values[values.length - 1]),
|
|
81
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Extension } from "@codemirror/state";
|
|
2
|
+
import { type StickyScrollOptions } from "./facet";
|
|
3
|
+
export type { StickyLine } from "./types";
|
|
4
|
+
export type { StickyScrollOptions, StickyScrollConfig } from "./facet";
|
|
5
|
+
export { stickyScrollFacet, defaultExcludeNode, makeStickyScrollConfig } from "./facet";
|
|
6
|
+
export { stickyScrollBaseTheme } from "./theme";
|
|
7
|
+
/**
|
|
8
|
+
* Add Monaco/VS Code-style sticky scroll to a CodeMirror 6 editor.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { stickyScroll } from "@fazelstudio/codemirror-stickyscroll";
|
|
12
|
+
*
|
|
13
|
+
* const view = new EditorView({
|
|
14
|
+
* extensions: [basicSetup, javascript(), stickyScroll({ maxStickyLines: 4 })],
|
|
15
|
+
* parent: el,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Note: the returned array deliberately does NOT contain any
|
|
20
|
+
* `syntaxHighlighting(...)` — token colors always come from the consumer's own
|
|
21
|
+
* theme (§4.4).
|
|
22
|
+
*/
|
|
23
|
+
export declare function stickyScroll(options?: StickyScrollOptions): Extension;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { stickyScrollFacet } from "./facet";
|
|
2
|
+
import { scrollStickyPlugin } from "./plugin";
|
|
3
|
+
import { stickyScrollBaseTheme } from "./theme";
|
|
4
|
+
export { stickyScrollFacet, defaultExcludeNode, makeStickyScrollConfig } from "./facet";
|
|
5
|
+
export { stickyScrollBaseTheme } from "./theme";
|
|
6
|
+
/**
|
|
7
|
+
* Add Monaco/VS Code-style sticky scroll to a CodeMirror 6 editor.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { stickyScroll } from "@fazelstudio/codemirror-stickyscroll";
|
|
11
|
+
*
|
|
12
|
+
* const view = new EditorView({
|
|
13
|
+
* extensions: [basicSetup, javascript(), stickyScroll({ maxStickyLines: 4 })],
|
|
14
|
+
* parent: el,
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Note: the returned array deliberately does NOT contain any
|
|
19
|
+
* `syntaxHighlighting(...)` — token colors always come from the consumer's own
|
|
20
|
+
* theme (§4.4).
|
|
21
|
+
*/
|
|
22
|
+
export function stickyScroll(options = {}) {
|
|
23
|
+
return [stickyScrollFacet.of(options), stickyScrollBaseTheme, scrollStickyPlugin];
|
|
24
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
|
2
|
+
import type { StickyLine } from "./types";
|
|
3
|
+
declare class StickyScrollPlugin {
|
|
4
|
+
private readonly dom;
|
|
5
|
+
private readonly inner;
|
|
6
|
+
private readonly view;
|
|
7
|
+
private config;
|
|
8
|
+
private readonly cache;
|
|
9
|
+
private rafHandle;
|
|
10
|
+
private observer;
|
|
11
|
+
private gutterObserver;
|
|
12
|
+
private gutterMutationObserver;
|
|
13
|
+
private gutterMetrics;
|
|
14
|
+
private currentHeight;
|
|
15
|
+
private lastLineKey;
|
|
16
|
+
get lineHeight(): number;
|
|
17
|
+
constructor(view: EditorView);
|
|
18
|
+
clearCache(): void;
|
|
19
|
+
update(update: ViewUpdate): void;
|
|
20
|
+
destroy(): void;
|
|
21
|
+
private onScroll;
|
|
22
|
+
private request;
|
|
23
|
+
private render;
|
|
24
|
+
private hide;
|
|
25
|
+
/**
|
|
26
|
+
* Rebuild all row DOM nodes (called only when line numbers change).
|
|
27
|
+
* We clear the inner wrapper and build fresh — no stale DOM element issues.
|
|
28
|
+
*/
|
|
29
|
+
private buildRows;
|
|
30
|
+
/**
|
|
31
|
+
* Update per-frame attributes WITHOUT rebuilding DOM.
|
|
32
|
+
* Called every frame when the line-set is unchanged.
|
|
33
|
+
*/
|
|
34
|
+
private updateRowAttrs;
|
|
35
|
+
/**
|
|
36
|
+
* Build a single sticky row element.
|
|
37
|
+
*
|
|
38
|
+
* The gutter overlay mirrors the real editor gutter column-by-column:
|
|
39
|
+
* .cm-stickyscroll-gutter (position:absolute, width = total gutter width)
|
|
40
|
+
*/
|
|
41
|
+
private makeRow;
|
|
42
|
+
setClass(row: HTMLDivElement, line: StickyLine): void;
|
|
43
|
+
setWidth(row: HTMLDivElement, gutter: HTMLDivElement): void;
|
|
44
|
+
setHeight(ele: HTMLDivElement): void;
|
|
45
|
+
setTransform(code: HTMLDivElement): void;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the sticky bar's background to a GUARANTEED-opaque color that
|
|
48
|
+
* matches the editor's effective backdrop.
|
|
49
|
+
*
|
|
50
|
+
* 1. Walk the real ancestor chain (contentDOM → … → <html>) and return the
|
|
51
|
+
* first opaque color, skipping fully transparent AND semi-transparent
|
|
52
|
+
* layers (a semi-transparent ancestor is only used as a last resort).
|
|
53
|
+
* 2. Fall back to a dark/light hex default.
|
|
54
|
+
*
|
|
55
|
+
* This makes it impossible for the sticky bar to end up see-through, exactly
|
|
56
|
+
* like Monaco's sticky widget which always paints a solid background.
|
|
57
|
+
*/
|
|
58
|
+
private resolveBackground;
|
|
59
|
+
private detectBg;
|
|
60
|
+
}
|
|
61
|
+
export declare const scrollStickyPlugin: ViewPlugin<StickyScrollPlugin, undefined>;
|
|
62
|
+
export {};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { language, syntaxTree } from "@codemirror/language";
|
|
2
|
+
import { Direction, EditorView, ViewPlugin } from "@codemirror/view";
|
|
3
|
+
import { getStickyContext } from "./compute";
|
|
4
|
+
import { stickyScrollFacet } from "./facet";
|
|
5
|
+
import { renderLineCode } from "./render";
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Utilities
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Alpha channel of a computed `background-color`, or `null` when the color has
|
|
11
|
+
* no alpha channel (hex / named / `rgb(...)` → fully opaque).
|
|
12
|
+
*/
|
|
13
|
+
function alphaOf(bg) {
|
|
14
|
+
if (!bg || bg === "transparent")
|
|
15
|
+
return 0;
|
|
16
|
+
const m = /rgba?\(([^)]*)\)/.exec(bg);
|
|
17
|
+
if (!m)
|
|
18
|
+
return 1;
|
|
19
|
+
const parts = m[1].split(",");
|
|
20
|
+
if (parts.length === 4) {
|
|
21
|
+
const a = parseFloat(parts[3]);
|
|
22
|
+
return Number.isNaN(a) ? 1 : a;
|
|
23
|
+
}
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
function measureGutters(view) {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
28
|
+
const guttersEl = view.dom.querySelector(".cm-gutters");
|
|
29
|
+
if (!guttersEl)
|
|
30
|
+
return { totalWidth: 0, columns: [] };
|
|
31
|
+
const totalWidth = guttersEl.offsetWidth;
|
|
32
|
+
const columns = Array.from(guttersEl.children, (child) => ({
|
|
33
|
+
width: child.offsetWidth,
|
|
34
|
+
className: child.className
|
|
35
|
+
}));
|
|
36
|
+
return { totalWidth, columns };
|
|
37
|
+
}
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// VSCode/Monaco-style sticky scroll implementation
|
|
40
|
+
//
|
|
41
|
+
// Reference: stickyScrollWidget.ts + stickyScrollController.ts
|
|
42
|
+
//
|
|
43
|
+
// State has two key values:
|
|
44
|
+
// • startLineNumbers[] — the N sticky lines to show (outermost → innermost)
|
|
45
|
+
// • lastLineRelativePosition — NEGATIVE in VSCode; how many px the LAST
|
|
46
|
+
// (innermost) line has been pushed UP by the
|
|
47
|
+
// approaching section break. Range (−lineHeight, 0].
|
|
48
|
+
//
|
|
49
|
+
// DOM layout (widget):
|
|
50
|
+
// rootDomNode (.sticky-widget) overflow:hidden; height set per-frame
|
|
51
|
+
// sticky-widget-lines the rows, absolutely positioned
|
|
52
|
+
// row × N each row has an explicit `top`
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
class StickyScrollPlugin {
|
|
55
|
+
get lineHeight() {
|
|
56
|
+
return Math.round(this.view.defaultLineHeight) || 16;
|
|
57
|
+
}
|
|
58
|
+
constructor(view) {
|
|
59
|
+
this.cache = new Map();
|
|
60
|
+
this.rafHandle = null;
|
|
61
|
+
this.observer = null;
|
|
62
|
+
this.gutterObserver = null;
|
|
63
|
+
this.gutterMutationObserver = null;
|
|
64
|
+
this.gutterMetrics = { totalWidth: 0, columns: [] };
|
|
65
|
+
this.currentHeight = 0;
|
|
66
|
+
this.lastLineKey = "";
|
|
67
|
+
this.onScroll = () => this.request();
|
|
68
|
+
this.view = view;
|
|
69
|
+
this.config = view.state.facet(stickyScrollFacet);
|
|
70
|
+
// Outer container — clips content, fixed at top of editor.
|
|
71
|
+
this.dom = document.createElement("div");
|
|
72
|
+
this.dom.className =
|
|
73
|
+
"cm-stickyscroll-container" +
|
|
74
|
+
(this.config.class ? ` ${this.config.class}` : "");
|
|
75
|
+
// Inner wrapper — holds the rows; each row is laid out in normal flow and
|
|
76
|
+
// the container clips at its bottom edge (VSCode slide-away).
|
|
77
|
+
this.inner = document.createElement("div");
|
|
78
|
+
this.inner.className = "cm-stickyscroll-inner";
|
|
79
|
+
this.dom.appendChild(this.inner);
|
|
80
|
+
view.dom.appendChild(this.dom);
|
|
81
|
+
view.scrollDOM.addEventListener("scroll", this.onScroll, { passive: true });
|
|
82
|
+
this.observer = new ResizeObserver(this.onScroll);
|
|
83
|
+
this.observer.observe(view.dom);
|
|
84
|
+
const gutterEl = view.dom.querySelector(".cm-gutters");
|
|
85
|
+
if (gutterEl) {
|
|
86
|
+
this.gutterObserver = new ResizeObserver(this.onScroll);
|
|
87
|
+
this.gutterObserver.observe(gutterEl);
|
|
88
|
+
this.gutterMutationObserver = new MutationObserver(() => {
|
|
89
|
+
this.clearCache();
|
|
90
|
+
this.request();
|
|
91
|
+
});
|
|
92
|
+
this.gutterMutationObserver.observe(gutterEl, { childList: true });
|
|
93
|
+
}
|
|
94
|
+
this.request();
|
|
95
|
+
}
|
|
96
|
+
clearCache() {
|
|
97
|
+
this.cache.clear();
|
|
98
|
+
this.lastLineKey = "";
|
|
99
|
+
}
|
|
100
|
+
update(update) {
|
|
101
|
+
const configChanged = update.state.facet(stickyScrollFacet) !== update.startState.facet(stickyScrollFacet);
|
|
102
|
+
this.config = update.state.facet(stickyScrollFacet);
|
|
103
|
+
const contentChanged = configChanged ||
|
|
104
|
+
update.docChanged ||
|
|
105
|
+
syntaxTree(update.startState) !== syntaxTree(update.state);
|
|
106
|
+
if (contentChanged) {
|
|
107
|
+
this.clearCache();
|
|
108
|
+
}
|
|
109
|
+
if (contentChanged ||
|
|
110
|
+
update.viewportChanged ||
|
|
111
|
+
update.geometryChanged ||
|
|
112
|
+
update.selectionSet ||
|
|
113
|
+
update.startState.facet(language) !== update.state.facet(language)) {
|
|
114
|
+
this.request();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
destroy() {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
if (this.rafHandle !== null)
|
|
120
|
+
cancelAnimationFrame(this.rafHandle);
|
|
121
|
+
this.view.scrollDOM.removeEventListener("scroll", this.onScroll);
|
|
122
|
+
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
123
|
+
(_b = this.gutterObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
|
|
124
|
+
this.dom.remove();
|
|
125
|
+
this.cache.clear();
|
|
126
|
+
}
|
|
127
|
+
request() {
|
|
128
|
+
if (this.rafHandle !== null)
|
|
129
|
+
return;
|
|
130
|
+
this.rafHandle = requestAnimationFrame(() => {
|
|
131
|
+
try {
|
|
132
|
+
this.render();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.warn("[codemirror-stickyscroll]", e);
|
|
136
|
+
}
|
|
137
|
+
this.rafHandle = null;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Main render — called every animation frame on scroll
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
143
|
+
render() {
|
|
144
|
+
const view = this.view;
|
|
145
|
+
// ── Measure ─────────────────────────────────────────────────────────────
|
|
146
|
+
this.gutterMetrics = measureGutters(view);
|
|
147
|
+
// ── Background (match active theme) ─────────────────────────────────────
|
|
148
|
+
// Always opaque (Monaco's sticky widget never lets the code show through).
|
|
149
|
+
this.dom.style.backgroundColor = this.resolveBackground();
|
|
150
|
+
// ── Compute sticky context ───────────────────────────────────────────────
|
|
151
|
+
const lines = getStickyContext(view, this.config);
|
|
152
|
+
if (!lines.length) {
|
|
153
|
+
this.hide();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// ── Rebuild rows when line-set changes ───────────────────────────────────
|
|
157
|
+
const lineKey = lines.map((l) => l.lineNumber).join("|");
|
|
158
|
+
if (lineKey !== this.lastLineKey) {
|
|
159
|
+
this.lastLineKey = lineKey;
|
|
160
|
+
this.buildRows(lines);
|
|
161
|
+
}
|
|
162
|
+
const lh = this.lineHeight;
|
|
163
|
+
const totalLines = lines.length;
|
|
164
|
+
// ── Apply layout math ────────────────────────────────────────────────────
|
|
165
|
+
//
|
|
166
|
+
// The container clips via overflow:hidden; we never translate the whole
|
|
167
|
+
// stack — that would make the OUTERMOST line disappear first, unlike VSCode.
|
|
168
|
+
const containerHeight = totalLines * lh;
|
|
169
|
+
this.currentHeight = containerHeight;
|
|
170
|
+
this.dom.style.height = `${containerHeight}px`;
|
|
171
|
+
this.dom.style.display = "";
|
|
172
|
+
// Apply per-frame row attributes (gutter metrics, scrollLeft and highlight).
|
|
173
|
+
this.updateRowAttrs(lines);
|
|
174
|
+
this.dom.dir = view.textDirection === Direction.RTL ? "rtl" : "ltr";
|
|
175
|
+
this.inner.style.lineHeight = `${lh}px`;
|
|
176
|
+
}
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
178
|
+
// DOM management
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
180
|
+
hide() {
|
|
181
|
+
this.dom.style.display = "none";
|
|
182
|
+
this.currentHeight = 0;
|
|
183
|
+
// Do NOT clear this.inner — keep rows for fast reuse on next show.
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Rebuild all row DOM nodes (called only when line numbers change).
|
|
187
|
+
* We clear the inner wrapper and build fresh — no stale DOM element issues.
|
|
188
|
+
*/
|
|
189
|
+
buildRows(lines) {
|
|
190
|
+
this.inner.textContent = ""; // Remove all children cleanly
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
this.inner.appendChild(this.makeRow(line));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Update per-frame attributes WITHOUT rebuilding DOM.
|
|
197
|
+
* Called every frame when the line-set is unchanged.
|
|
198
|
+
*/
|
|
199
|
+
updateRowAttrs(lines) {
|
|
200
|
+
const gm = this.gutterMetrics;
|
|
201
|
+
const rowEls = this.inner.children;
|
|
202
|
+
for (let i = 0; i < lines.length && i < rowEls.length; i++) {
|
|
203
|
+
const line = lines[i];
|
|
204
|
+
const row = rowEls[i];
|
|
205
|
+
this.setHeight(row);
|
|
206
|
+
this.setClass(row, line);
|
|
207
|
+
const gutter = row.firstElementChild;
|
|
208
|
+
this.setWidth(row, gutter);
|
|
209
|
+
this.setHeight(gutter);
|
|
210
|
+
const cols = gutter.children;
|
|
211
|
+
for (let c = 0; c < gm.columns.length && c < cols.length; c++) {
|
|
212
|
+
cols[c].style.width = `${gm.columns[c].width}px`;
|
|
213
|
+
}
|
|
214
|
+
const code = row.lastElementChild;
|
|
215
|
+
this.setTransform(code);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build a single sticky row element.
|
|
220
|
+
*
|
|
221
|
+
* The gutter overlay mirrors the real editor gutter column-by-column:
|
|
222
|
+
* .cm-stickyscroll-gutter (position:absolute, width = total gutter width)
|
|
223
|
+
*/
|
|
224
|
+
makeRow(line) {
|
|
225
|
+
const view = this.view;
|
|
226
|
+
const gm = this.gutterMetrics;
|
|
227
|
+
// Row
|
|
228
|
+
const row = document.createElement("div");
|
|
229
|
+
row.className = "cm-stickyscroll-line";
|
|
230
|
+
this.setClass(row, line);
|
|
231
|
+
row.setAttribute("role", "button");
|
|
232
|
+
row.setAttribute("tabindex", "0");
|
|
233
|
+
row.setAttribute("aria-label", `Go to line ${line.lineNumber}`);
|
|
234
|
+
this.setHeight(row);
|
|
235
|
+
// Gutter overlay container
|
|
236
|
+
const gutter = document.createElement("div");
|
|
237
|
+
gutter.className = "cm-stickyscroll-gutter";
|
|
238
|
+
this.setWidth(row, gutter);
|
|
239
|
+
this.setHeight(gutter);
|
|
240
|
+
if (gm.columns.length) {
|
|
241
|
+
gm.columns.forEach((colMetrics) => {
|
|
242
|
+
const col = document.createElement("div");
|
|
243
|
+
// Reuse the exact same classes so any custom user CSS (like Notron's .cm-lineNumbers .cm-gutterElement) matches!
|
|
244
|
+
col.className = colMetrics.className;
|
|
245
|
+
col.style.width = `${colMetrics.width}px`;
|
|
246
|
+
if (colMetrics.className.includes("cm-lineNumbers")) {
|
|
247
|
+
// Wrap in cm-gutterElement to perfectly match the DOM structure of CM6's line numbers.
|
|
248
|
+
const el = document.createElement("div");
|
|
249
|
+
el.className = "cm-gutterElement";
|
|
250
|
+
el.textContent = String(line.lineNumber);
|
|
251
|
+
col.appendChild(el);
|
|
252
|
+
}
|
|
253
|
+
gutter.appendChild(col);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Code span
|
|
257
|
+
const code = renderLineCode(view, line, this.cache);
|
|
258
|
+
this.setTransform(code);
|
|
259
|
+
row.appendChild(gutter);
|
|
260
|
+
row.appendChild(code);
|
|
261
|
+
// Click / keyboard
|
|
262
|
+
const jump = (e) => {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
view.dispatch({
|
|
266
|
+
selection: { anchor: line.from },
|
|
267
|
+
effects: EditorView.scrollIntoView(line.from, {
|
|
268
|
+
y: "start",
|
|
269
|
+
yMargin: this.currentHeight + 4,
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
view.focus();
|
|
273
|
+
};
|
|
274
|
+
row.addEventListener("click", jump);
|
|
275
|
+
row.addEventListener("keydown", (ev) => {
|
|
276
|
+
if (ev.key === "Enter" || ev.key === " ")
|
|
277
|
+
jump(ev);
|
|
278
|
+
});
|
|
279
|
+
return row;
|
|
280
|
+
}
|
|
281
|
+
setClass(row, line) {
|
|
282
|
+
const head = this.view.state.selection.main.head;
|
|
283
|
+
row.classList.toggle("cm-stickyscroll-current", head >= line.nodeFrom && head <= line.nodeTo);
|
|
284
|
+
}
|
|
285
|
+
setWidth(row, gutter) {
|
|
286
|
+
const gw = this.gutterMetrics.totalWidth;
|
|
287
|
+
row.style.paddingInlineStart = `${gw}px`;
|
|
288
|
+
gutter.style.width = `${gw}px`;
|
|
289
|
+
}
|
|
290
|
+
setHeight(ele) {
|
|
291
|
+
const lh = this.lineHeight;
|
|
292
|
+
ele.style.height = `${lh}px`;
|
|
293
|
+
}
|
|
294
|
+
setTransform(code) {
|
|
295
|
+
const scrollX = this.view.scrollDOM.scrollLeft;
|
|
296
|
+
code.style.transform = scrollX > 0 ? `translateX(${-scrollX}px)` : "";
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Resolve the sticky bar's background to a GUARANTEED-opaque color that
|
|
300
|
+
* matches the editor's effective backdrop.
|
|
301
|
+
*
|
|
302
|
+
* 1. Walk the real ancestor chain (contentDOM → … → <html>) and return the
|
|
303
|
+
* first opaque color, skipping fully transparent AND semi-transparent
|
|
304
|
+
* layers (a semi-transparent ancestor is only used as a last resort).
|
|
305
|
+
* 2. Fall back to a dark/light hex default.
|
|
306
|
+
*
|
|
307
|
+
* This makes it impossible for the sticky bar to end up see-through, exactly
|
|
308
|
+
* like Monaco's sticky widget which always paints a solid background.
|
|
309
|
+
*/
|
|
310
|
+
resolveBackground() {
|
|
311
|
+
const detected = this.detectBg();
|
|
312
|
+
if (detected)
|
|
313
|
+
return detected;
|
|
314
|
+
const isDark = this.view.dom.classList.contains("cm-dark");
|
|
315
|
+
return isDark ? "#1e1e1e" : "#fff";
|
|
316
|
+
}
|
|
317
|
+
detectBg() {
|
|
318
|
+
let semiTransparent = null;
|
|
319
|
+
let el = this.view.contentDOM;
|
|
320
|
+
while (el) {
|
|
321
|
+
const bg = getComputedStyle(el).backgroundColor;
|
|
322
|
+
const alpha = alphaOf(bg);
|
|
323
|
+
if (alpha > 0) {
|
|
324
|
+
if (alpha >= 1)
|
|
325
|
+
return bg;
|
|
326
|
+
semiTransparent = semiTransparent || bg;
|
|
327
|
+
}
|
|
328
|
+
el = el.parentElement;
|
|
329
|
+
}
|
|
330
|
+
return semiTransparent;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export const scrollStickyPlugin = ViewPlugin.fromClass(StickyScrollPlugin, {});
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditorView } from "@codemirror/view";
|
|
2
|
+
import type { StickyLine } from "./types";
|
|
3
|
+
/** Cache of rendered code elements keyed by opening line number. */
|
|
4
|
+
export type RowCache = Map<number, {
|
|
5
|
+
text: string;
|
|
6
|
+
el: HTMLDivElement;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Build (or return cached) a `<div class="cm-stickyscroll-code">` element
|
|
10
|
+
* containing the syntax-highlighted code for one opening line (§4.4 / §4.6b).
|
|
11
|
+
*
|
|
12
|
+
* Priority:
|
|
13
|
+
* 1. Deep-clone the live CM6 `.cm-line` element when it is in the viewport —
|
|
14
|
+
* token classes (`.tok-*`) are reused verbatim, guaranteed theme-accurate.
|
|
15
|
+
* 2. Re-highlight via the consumer's active `HighlightStyle`(s) using
|
|
16
|
+
* `highlightingFor()` — never our own color palette.
|
|
17
|
+
* 3. Plain-text fallback (leading whitespace/indentation preserved).
|
|
18
|
+
*
|
|
19
|
+
* Results are cached by `(lineNumber, text)` so unchanged lines are free.
|
|
20
|
+
* The cache must be cleared externally when the syntax tree or config changes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderLineCode(view: EditorView, line: StickyLine, cache: RowCache): HTMLDivElement;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { highlightingFor, language } from "@codemirror/language";
|
|
2
|
+
import { highlightTree } from "@lezer/highlight";
|
|
3
|
+
import { syntaxTree } from "@codemirror/language";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Internal: consumer-theme-aware Highlighter
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Resolves highlight classes from the *active* styles in the consumer's
|
|
9
|
+
* EditorState (§4.4) — not from any palette bundled with this package.
|
|
10
|
+
*/
|
|
11
|
+
class StateHighlighter {
|
|
12
|
+
constructor(state, topNodeType) {
|
|
13
|
+
this.state = state;
|
|
14
|
+
this.topNodeType = topNodeType;
|
|
15
|
+
}
|
|
16
|
+
style(tags) {
|
|
17
|
+
return highlightingFor(this.state, tags, this.topNodeType);
|
|
18
|
+
}
|
|
19
|
+
scope() {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Internal: find the live `.cm-line` DOM element for a document line
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function findRenderedLineElement(view, lineNumber) {
|
|
27
|
+
const lineFrom = view.state.doc.line(lineNumber).from;
|
|
28
|
+
// Only lines that are actually rendered in the DOM can be cloned. Anything
|
|
29
|
+
// outside `view.viewport` (visible area + render margin) must fall back to
|
|
30
|
+
// re-highlighting, never to a position-clamped lookup.
|
|
31
|
+
if (lineFrom < view.viewport.from || lineFrom > view.viewport.to)
|
|
32
|
+
return null;
|
|
33
|
+
// `domAtPos` maps a document position to its exact DOM node, so it is immune
|
|
34
|
+
// to index misalignment between `viewportLineBlocks` and `querySelectorAll`.
|
|
35
|
+
// (Block widgets, placeholder lines and render-margin lines make those two
|
|
36
|
+
// lists diverge, which previously caused wrong — blank / comment / `}` —
|
|
37
|
+
// line text to be cloned into the sticky bar.)
|
|
38
|
+
let dom;
|
|
39
|
+
try {
|
|
40
|
+
dom = view.domAtPos(lineFrom, 1);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
let el = dom.node instanceof HTMLElement ? dom.node : dom.node.parentElement;
|
|
46
|
+
while (el && !el.classList.contains("cm-line"))
|
|
47
|
+
el = el.parentElement;
|
|
48
|
+
return el;
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Public: renderLineCode
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Build (or return cached) a `<div class="cm-stickyscroll-code">` element
|
|
55
|
+
* containing the syntax-highlighted code for one opening line (§4.4 / §4.6b).
|
|
56
|
+
*
|
|
57
|
+
* Priority:
|
|
58
|
+
* 1. Deep-clone the live CM6 `.cm-line` element when it is in the viewport —
|
|
59
|
+
* token classes (`.tok-*`) are reused verbatim, guaranteed theme-accurate.
|
|
60
|
+
* 2. Re-highlight via the consumer's active `HighlightStyle`(s) using
|
|
61
|
+
* `highlightingFor()` — never our own color palette.
|
|
62
|
+
* 3. Plain-text fallback (leading whitespace/indentation preserved).
|
|
63
|
+
*
|
|
64
|
+
* Results are cached by `(lineNumber, text)` so unchanged lines are free.
|
|
65
|
+
* The cache must be cleared externally when the syntax tree or config changes.
|
|
66
|
+
*/
|
|
67
|
+
export function renderLineCode(view, line, cache) {
|
|
68
|
+
// Cache hit.
|
|
69
|
+
const hit = cache.get(line.lineNumber);
|
|
70
|
+
if (hit && hit.text === line.text)
|
|
71
|
+
return hit.el;
|
|
72
|
+
const makeSpan = (child) => {
|
|
73
|
+
const s = document.createElement("div");
|
|
74
|
+
s.className = "cm-stickyscroll-code";
|
|
75
|
+
if (child)
|
|
76
|
+
s.appendChild(child);
|
|
77
|
+
else
|
|
78
|
+
s.textContent = line.text; // plain text, indentation preserved
|
|
79
|
+
return s;
|
|
80
|
+
};
|
|
81
|
+
// ── Strategy 1: clone the live viewport DOM line ─────────────────────────
|
|
82
|
+
const rendered = findRenderedLineElement(view, line.lineNumber);
|
|
83
|
+
// Ensure we don't clone empty virtual DOM placeholders that CM uses during fast scrolls.
|
|
84
|
+
if (rendered && rendered.textContent !== "") {
|
|
85
|
+
const frag = document.createDocumentFragment();
|
|
86
|
+
for (const child of rendered.childNodes) {
|
|
87
|
+
frag.appendChild(child.cloneNode(true));
|
|
88
|
+
}
|
|
89
|
+
const el = makeSpan(frag);
|
|
90
|
+
cache.set(line.lineNumber, { text: line.text, el });
|
|
91
|
+
return el;
|
|
92
|
+
}
|
|
93
|
+
// ── Strategy 2: re-highlight from the consumer's active HighlightStyle(s) ─
|
|
94
|
+
const lang = view.state.facet(language);
|
|
95
|
+
if (lang) {
|
|
96
|
+
try {
|
|
97
|
+
const tree = syntaxTree(view.state);
|
|
98
|
+
const topType = tree.type;
|
|
99
|
+
const highlighters = [new StateHighlighter(view.state, topType)];
|
|
100
|
+
const out = document.createElement("div");
|
|
101
|
+
out.className = "cm-stickyscroll-code";
|
|
102
|
+
let last = line.from;
|
|
103
|
+
highlightTree(tree, highlighters, (from, to, cls) => {
|
|
104
|
+
const start = Math.max(last, from);
|
|
105
|
+
const end = Math.min(line.to, to);
|
|
106
|
+
if (start >= end)
|
|
107
|
+
return;
|
|
108
|
+
if (start > last) {
|
|
109
|
+
out.appendChild(document.createTextNode(line.text.slice(last - line.from, start - line.from)));
|
|
110
|
+
}
|
|
111
|
+
if (cls) {
|
|
112
|
+
const tok = document.createElement("span");
|
|
113
|
+
tok.className = cls;
|
|
114
|
+
tok.textContent = line.text.slice(start - line.from, end - line.from);
|
|
115
|
+
out.appendChild(tok);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
out.appendChild(document.createTextNode(line.text.slice(start - line.from, end - line.from)));
|
|
119
|
+
}
|
|
120
|
+
last = end;
|
|
121
|
+
}, line.from, line.to);
|
|
122
|
+
// Trailing unstyled text.
|
|
123
|
+
if (last < line.to) {
|
|
124
|
+
out.appendChild(document.createTextNode(line.text.slice(last - line.from)));
|
|
125
|
+
}
|
|
126
|
+
// Only cache when we produced actual content.
|
|
127
|
+
if (out.childNodes.length > 0) {
|
|
128
|
+
cache.set(line.lineNumber, { text: line.text, el: out });
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Fall through to plain text.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── Strategy 3: plain text ────────────────────────────────────────────────
|
|
137
|
+
const el = makeSpan();
|
|
138
|
+
cache.set(line.lineNumber, { text: line.text, el });
|
|
139
|
+
return el;
|
|
140
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cosmetic-only theme for the sticky bar.
|
|
3
|
+
*
|
|
4
|
+
* ALL sizing (height, lineHeight, padding, overflow, position, display,
|
|
5
|
+
* transform) is set via inline style by plugin.ts — matching Monaco's exact
|
|
6
|
+
* approach of updating styles imperatively on every scroll event.
|
|
7
|
+
*
|
|
8
|
+
* This file only provides:
|
|
9
|
+
* 1. Background color fallback (JS overrides immediately with actual editor bg)
|
|
10
|
+
* 2. Current-scope highlight
|
|
11
|
+
* 3. Gutter cosmetics (color, opacity)
|
|
12
|
+
* 4. Dark-mode override for background fallback
|
|
13
|
+
*
|
|
14
|
+
* ⛔ Do NOT add: height, lineHeight, overflow, display, padding, margin,
|
|
15
|
+
* position, transform to .cm-stickyscroll-line or .cm-stickyscroll-inner.
|
|
16
|
+
* Plugin.ts owns those — any CSS here will conflict.
|
|
17
|
+
* ⛔ Do NOT add syntaxHighlighting — token colors come from the consumer's theme.
|
|
18
|
+
*/
|
|
19
|
+
export declare const stickyScrollBaseTheme: import("@codemirror/state").Extension;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
/**
|
|
3
|
+
* Cosmetic-only theme for the sticky bar.
|
|
4
|
+
*
|
|
5
|
+
* ALL sizing (height, lineHeight, padding, overflow, position, display,
|
|
6
|
+
* transform) is set via inline style by plugin.ts — matching Monaco's exact
|
|
7
|
+
* approach of updating styles imperatively on every scroll event.
|
|
8
|
+
*
|
|
9
|
+
* This file only provides:
|
|
10
|
+
* 1. Background color fallback (JS overrides immediately with actual editor bg)
|
|
11
|
+
* 2. Current-scope highlight
|
|
12
|
+
* 3. Gutter cosmetics (color, opacity)
|
|
13
|
+
* 4. Dark-mode override for background fallback
|
|
14
|
+
*
|
|
15
|
+
* ⛔ Do NOT add: height, lineHeight, overflow, display, padding, margin,
|
|
16
|
+
* position, transform to .cm-stickyscroll-line or .cm-stickyscroll-inner.
|
|
17
|
+
* Plugin.ts owns those — any CSS here will conflict.
|
|
18
|
+
* ⛔ Do NOT add syntaxHighlighting — token colors come from the consumer's theme.
|
|
19
|
+
*/
|
|
20
|
+
export const stickyScrollBaseTheme = EditorView.baseTheme({
|
|
21
|
+
// Container background fallback — JS overrides with exact editor color.
|
|
22
|
+
".cm-stickyscroll-container": {
|
|
23
|
+
position: "absolute",
|
|
24
|
+
top: 0,
|
|
25
|
+
left: 0,
|
|
26
|
+
right: 0,
|
|
27
|
+
zIndex: 10,
|
|
28
|
+
overflow: "hidden",
|
|
29
|
+
boxSizing: "border-box",
|
|
30
|
+
fontFamily: "monospace",
|
|
31
|
+
tabSize: 4,
|
|
32
|
+
borderBottom: "1px solid rgba(128,128,128,.2)",
|
|
33
|
+
boxShadow: "0 2px 4px var(--cm-stickyscroll-shadow)",
|
|
34
|
+
},
|
|
35
|
+
"&light .cm-stickyscroll-container": {
|
|
36
|
+
"--cm-stickyscroll-shadow": "rgba(0,0,0,.12)",
|
|
37
|
+
},
|
|
38
|
+
"&dark .cm-stickyscroll-container": {
|
|
39
|
+
"--cm-stickyscroll-shadow": "rgba(255,255,255,.12)",
|
|
40
|
+
},
|
|
41
|
+
".cm-stickyscroll-line": {
|
|
42
|
+
position: "relative",
|
|
43
|
+
overflow: "hidden",
|
|
44
|
+
whiteSpace: "pre",
|
|
45
|
+
boxSizing: "border-box",
|
|
46
|
+
cursor: "pointer",
|
|
47
|
+
// Current scope indicator (left accent bar — same as VSCode).
|
|
48
|
+
"&.cm-stickyscroll-current": {
|
|
49
|
+
backgroundColor: "rgba(128,128,128,.07)",
|
|
50
|
+
boxShadow: "inset 3px 0 0 #4b9edd",
|
|
51
|
+
},
|
|
52
|
+
"&:hover": {
|
|
53
|
+
backgroundColor: "rgba(128,128,128,.09)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// Gutter cosmetics — position/size are inline from plugin.ts.
|
|
57
|
+
".cm-stickyscroll-gutter": {
|
|
58
|
+
position: "absolute",
|
|
59
|
+
top: 0,
|
|
60
|
+
insetInlineStart: 0,
|
|
61
|
+
zIndex: 1,
|
|
62
|
+
display: "flex",
|
|
63
|
+
alignItems: "stretch",
|
|
64
|
+
boxSizing: "border-box",
|
|
65
|
+
WebkitUserSelect: "none",
|
|
66
|
+
userSelect: "none",
|
|
67
|
+
borderInlineEnd: "1px solid rgba(128,128,128,.18)",
|
|
68
|
+
opacity: 0.55,
|
|
69
|
+
},
|
|
70
|
+
".cm-stickyscroll-code": {
|
|
71
|
+
padding: "0 2px 0 6px",
|
|
72
|
+
},
|
|
73
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single sticky breadcrumb row shown in the sticky bar.
|
|
3
|
+
*
|
|
4
|
+
* Each entry describes the *opening line* of a scope block (function, class,
|
|
5
|
+
* if/loop block, ...) whose opening line has scrolled out above the viewport
|
|
6
|
+
* while its closing line is still at/below the viewport.
|
|
7
|
+
*/
|
|
8
|
+
export interface StickyLine {
|
|
9
|
+
/** 1-based line number of the opening line. */
|
|
10
|
+
lineNumber: number;
|
|
11
|
+
/** Position of the start of the opening line. Used for click-to-jump + selection. */
|
|
12
|
+
from: number;
|
|
13
|
+
/** Position of the end of the opening line. */
|
|
14
|
+
to: number;
|
|
15
|
+
/** Full text of the opening line (leading indentation preserved). */
|
|
16
|
+
text: string;
|
|
17
|
+
/** Start position of the whole foldable scope node (e.g. `function` keyword). */
|
|
18
|
+
nodeFrom: number;
|
|
19
|
+
/** End position of the whole foldable scope node (e.g. closing brace). */
|
|
20
|
+
nodeTo: number;
|
|
21
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhsd/codemirror-stickyscroll",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VS Code / Monaco-style sticky scroll (sticky lines) extension for CodeMirror 6",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codemirror",
|
|
7
|
+
"sticky"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/bhsd-harry/stickyscroll#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/bhsd-harry/stickyscroll/issues"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"contributors": [
|
|
15
|
+
"[Zulfazli (Fazelllyyy)](https://github.com/fazelllyyy)",
|
|
16
|
+
"Bhsd"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/bhsd-harry/codemirror-stickyscroll.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"files": [
|
|
24
|
+
"/dist/"
|
|
25
|
+
],
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"scripts": {
|
|
30
|
+
"prepublishOnly": "npm run build",
|
|
31
|
+
"lint:ts": "tsc --noEmit && eslint --cache .",
|
|
32
|
+
"lint:md": "markdownlint-cli2 '**/*.md'",
|
|
33
|
+
"lint:css": "stylelint --cache '**/*.css'",
|
|
34
|
+
"lint": "npm run lint:ts && npm run lint:css && npm run lint:md",
|
|
35
|
+
"build": "tsc && eslint --no-inline-config --no-config-lookup -c eslint.dist.js dist/*.js"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@codemirror/language": "^6.3.0",
|
|
39
|
+
"@codemirror/state": "^6.0.0",
|
|
40
|
+
"@codemirror/view": "^6.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@bhsd/code-standard": "^4.1.0",
|
|
44
|
+
"@codemirror/language": "^6.12.4",
|
|
45
|
+
"@codemirror/state": "^6.7.1",
|
|
46
|
+
"@codemirror/view": "^6.43.9",
|
|
47
|
+
"eslint": "^10.9.1",
|
|
48
|
+
"markdownlint-cli2": "^0.23.2",
|
|
49
|
+
"stylelint": "^17.14.1",
|
|
50
|
+
"typescript": "^6.0.3"
|
|
51
|
+
}
|
|
52
|
+
}
|