@bhsd/codemirror-stickyscroll 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -27
- package/dist/compute.js +24 -19
- package/dist/facet.d.ts +0 -2
- package/dist/facet.js +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -2
- package/dist/render.js +1 -10
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> VS Code / Monaco-style **sticky scroll** (sticky lines) for [CodeMirror 6](https://codemirror.net/).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
This is a fork of [@fazelstudio/codemirror-stickyscroll](https://github.com/fazel-studio/codemirror-stickyscroll).
|
|
5
|
+
This is a fork of [@fazelstudio/codemirror-stickyscroll](https://github.com/fazelstudio/codemirror-stickyscroll)
|
|
6
|
+
that adds support for [foldService](https://codemirror.net/docs/ref/#language.foldService),
|
|
7
|
+
which makes it compatible with legacy [StreamParser](https://codemirror.net/docs/ref/#language.StreamParser)
|
|
8
|
+
languages that register folding. This personal fork also removes some unused features.
|
|
10
9
|
|
|
11
10
|
Sticky lines keep the *opening* lines of the enclosing scopes (function, class,
|
|
12
11
|
if/loop blocks, …) pinned at the top of the editor while you scroll — exactly
|
|
@@ -19,16 +18,17 @@ extension** (no fork of `@codemirror/*`).
|
|
|
19
18
|
throttle keeps the bar glued to the scroll position; no "jumpy" updates.
|
|
20
19
|
- **Click-to-jump with margin compensation** — clicking a sticky line scrolls
|
|
21
20
|
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`) +
|
|
21
|
+
jump to is never hidden behind the bar. Keyboard (`Enter`/`Space`) +
|
|
22
|
+
`role="button"` for a11y.
|
|
23
23
|
- **Reuses the consumer's theme** — the bar re-highlights lines through the
|
|
24
24
|
*active* highlight styles of the editor (`highlightingFor`), or clones the
|
|
25
25
|
already-rendered DOM line when available. The package **never** registers its
|
|
26
26
|
own `syntaxHighlighting(...)`.
|
|
27
|
-
- **Language-agnostic** — detection is based on `
|
|
28
|
-
`@codemirror/language` (the fold services / fold node props that every
|
|
27
|
+
- **Language-agnostic** — detection is based on `foldNodeProp` or `foldService`
|
|
28
|
+
from `@codemirror/language` (the fold services / fold node props that every
|
|
29
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
|
|
31
|
-
and gracefully handles data languages like JSON.
|
|
30
|
+
that works across multiple languages (JS/TS, Python, Rust, Go, etc.) out of
|
|
31
|
+
the box, and gracefully handles data languages like JSON.
|
|
32
32
|
- **Gutter alignment, horizontal sync, RTL, resize-proof** — line numbers are
|
|
33
33
|
aligned with the real gutter (width tracked via `ResizeObserver`), the bar
|
|
34
34
|
follows horizontal scroll, and it reacts to font/zoom/resize changes.
|
|
@@ -45,11 +45,9 @@ has a working CodeMirror 6 editor):
|
|
|
45
45
|
|
|
46
46
|
| Package | Minimum |
|
|
47
47
|
| --- | --- |
|
|
48
|
-
| `@codemirror/view` | ^6.
|
|
48
|
+
| `@codemirror/view` | ^6.3.0 |
|
|
49
49
|
| `@codemirror/state` | ^6.0.0 |
|
|
50
50
|
| `@codemirror/language` | ^6.0.0 |
|
|
51
|
-
| `@lezer/common` | ^1.0.0 |
|
|
52
|
-
| `@lezer/highlight` | ^1.0.0 |
|
|
53
51
|
|
|
54
52
|
## API
|
|
55
53
|
|
|
@@ -59,7 +57,7 @@ has a working CodeMirror 6 editor):
|
|
|
59
57
|
interface StickyScrollOptions {
|
|
60
58
|
/** Maximum sticky lines shown at once (dynamically clamped to ~40% of editor height). Default: 4 */
|
|
61
59
|
maxStickyLines?: number;
|
|
62
|
-
/** Minimum lines a scope must span to become sticky. Default:
|
|
60
|
+
/** Minimum lines a scope must span to become sticky. Default: 6 */
|
|
63
61
|
minBlockLines?: number;
|
|
64
62
|
/**
|
|
65
63
|
* Denylist predicate: return true to never pin a foldable node of that type.
|
|
@@ -67,11 +65,7 @@ interface StickyScrollOptions {
|
|
|
67
65
|
* It uses exact node names for JS/TS and generic regex patterns for other
|
|
68
66
|
* languages, while automatically bypassing the literal denylist for JSON.
|
|
69
67
|
*/
|
|
70
|
-
excludeNode?: (nodeName
|
|
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;
|
|
68
|
+
excludeNode?: (nodeName?: string, langName?: string, ownerName?: string) => boolean;
|
|
75
69
|
/** Extra CSS class(es) for the bar container. */
|
|
76
70
|
class?: string;
|
|
77
71
|
}
|
|
@@ -82,12 +76,8 @@ interface StickyScrollOptions {
|
|
|
82
76
|
```ts
|
|
83
77
|
import {
|
|
84
78
|
stickyScroll,
|
|
85
|
-
stickyScrollFacet, // the configuration Facet (compose/override per instance)
|
|
86
79
|
defaultExcludeNode, // default denylist implementation
|
|
87
|
-
makeStickyScrollConfig, // merge options with defaults
|
|
88
|
-
stickyScrollBaseTheme, // layout-only base theme (no token colors)
|
|
89
80
|
type StickyScrollOptions,
|
|
90
|
-
type StickyLine,
|
|
91
81
|
} from "@bhsd/codemirror-stickyscroll";
|
|
92
82
|
```
|
|
93
83
|
|
|
@@ -105,15 +95,14 @@ Per-row hooks:
|
|
|
105
95
|
| `.cm-stickyscroll-line:hover` | hover highlight |
|
|
106
96
|
| `.cm-stickyscroll-line.cm-stickyscroll-current` | block containing the cursor |
|
|
107
97
|
| `.cm-stickyscroll-gutter` | line-number cell |
|
|
108
|
-
| `.cm-stickyscroll-
|
|
98
|
+
| `.cm-stickyscroll-code` | code column (horizontally synced) |
|
|
109
99
|
|
|
110
100
|
## How it works
|
|
111
101
|
|
|
112
|
-
1. **Detect the top line** — `view.lineBlockAtHeight(
|
|
102
|
+
1. **Detect the top line** — `view.lineBlockAtHeight(clientTop - documentTop)`.
|
|
113
103
|
2. **Walk the syntax tree** — from `syntaxTree(state).resolveInner(topPos, 0)`
|
|
114
104
|
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
|
|
105
|
+
3. **A scope qualifies when** its opening line is *foldable*, its fold
|
|
117
106
|
anchor node is not denylisted, and it spans ≥ `minBlockLines`.
|
|
118
107
|
4. **Render** — a `ViewPlugin` owns an absolutely-positioned overlay
|
|
119
108
|
(`position: absolute; top: 0` inside `view.dom`), updated per-frame from the
|
package/dist/compute.js
CHANGED
|
@@ -7,6 +7,11 @@ import { ensureSyntaxTree, foldNodeProp, foldService, language, syntaxTree } fro
|
|
|
7
7
|
* reveal; once the tree covers the position the cost is O(1).
|
|
8
8
|
*/
|
|
9
9
|
const PARSE_TIMEOUT = 25;
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of lines to scan for foldable blocks when any fold service is
|
|
12
|
+
* registered.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_SERVICE_SCAN_LINES = 100;
|
|
10
15
|
/**
|
|
11
16
|
* Return the best syntax tree we can answer a query at `pos` with right now.
|
|
12
17
|
*
|
|
@@ -57,7 +62,7 @@ export function getStickyContext(view, config) {
|
|
|
57
62
|
const lineHeight = Math.round(view.defaultLineHeight) || 16;
|
|
58
63
|
const maxDynamic = Math.max(1, Math.floor(view.scrollDOM.clientHeight * 0.4 / lineHeight));
|
|
59
64
|
const maxSticky = Math.min(config.maxStickyLines, maxDynamic);
|
|
60
|
-
return getStickyContextForRange(
|
|
65
|
+
return getStickyContextForRange(state, topBlock.from, config, parseTree, maxSticky);
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Pure version of the algorithm.
|
|
@@ -66,7 +71,7 @@ export function getStickyContext(view, config) {
|
|
|
66
71
|
* already-extended tree (see `getStickyContext`) to compute against a parse
|
|
67
72
|
* that is known to cover `fromPos`.
|
|
68
73
|
*/
|
|
69
|
-
function getStickyContextForRange(
|
|
74
|
+
function getStickyContextForRange(state, fromPos, config, t, maxSticky) {
|
|
70
75
|
const lang = state.facet(language);
|
|
71
76
|
if (!lang)
|
|
72
77
|
return [];
|
|
@@ -129,28 +134,28 @@ function getStickyContextForRange({ state, viewport }, fromPos, config, t, maxSt
|
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
else {
|
|
132
|
-
|
|
137
|
+
const minLineNumber = Math.max(0, topLineNumber - MAX_SERVICE_SCAN_LINES);
|
|
138
|
+
for (let openLine = topLineNumber - 1; openLine > minLineNumber; openLine--) {
|
|
133
139
|
const open = doc.line(openLine);
|
|
134
|
-
if (open.to < viewport.from)
|
|
135
|
-
break;
|
|
136
140
|
const ownerName = open.text.trim();
|
|
137
141
|
if (!exclude(undefined, langName, ownerName)) {
|
|
138
142
|
for (const service of services) {
|
|
139
143
|
const foldRange = service(state, open.from, open.to);
|
|
140
|
-
if (foldRange) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
144
|
+
if (!foldRange) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const nodeTo = Math.min(foldRange.to, doc.length);
|
|
148
|
+
const close = doc.lineAt(nodeTo);
|
|
149
|
+
const closeLine = close.number;
|
|
150
|
+
if (closeLine - openLine + 1 >= minBlockLines && closeLine >= topLineNumber) {
|
|
151
|
+
found.push({
|
|
152
|
+
lineNumber: openLine,
|
|
153
|
+
from: open.from,
|
|
154
|
+
to: open.to,
|
|
155
|
+
text: open.text,
|
|
156
|
+
nodeFrom: foldRange.from,
|
|
157
|
+
nodeTo,
|
|
158
|
+
});
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
}
|
package/dist/facet.d.ts
CHANGED
|
@@ -35,8 +35,6 @@ export interface StickyScrollConfig extends Required<Pick<StickyScrollOptions, "
|
|
|
35
35
|
* for a language you need exact behavior on.
|
|
36
36
|
*/
|
|
37
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
38
|
/**
|
|
41
39
|
* The facet carrying the sticky-scroll configuration.
|
|
42
40
|
*
|
package/dist/facet.js
CHANGED
|
@@ -59,7 +59,7 @@ export const defaultExcludeNode = (nodeName, langName, ownerName) => {
|
|
|
59
59
|
const DEFAULT_MAX_STICKY_LINES = 4;
|
|
60
60
|
const DEFAULT_MIN_BLOCK_LINES = 6;
|
|
61
61
|
/** Merge partial options with built-in defaults. */
|
|
62
|
-
|
|
62
|
+
function makeStickyScrollConfig(options) {
|
|
63
63
|
var _a, _b, _c;
|
|
64
64
|
return {
|
|
65
65
|
maxStickyLines: (_a = options === null || options === void 0 ? void 0 : options.maxStickyLines) !== null && _a !== void 0 ? _a : DEFAULT_MAX_STICKY_LINES,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { Extension } from "@codemirror/state";
|
|
2
2
|
import { type StickyScrollOptions } from "./facet";
|
|
3
|
-
export type {
|
|
4
|
-
export
|
|
5
|
-
export { stickyScrollFacet, defaultExcludeNode, makeStickyScrollConfig } from "./facet";
|
|
6
|
-
export { stickyScrollBaseTheme } from "./theme";
|
|
3
|
+
export type { StickyScrollOptions } from "./facet";
|
|
4
|
+
export { defaultExcludeNode } from "./facet";
|
|
7
5
|
/**
|
|
8
6
|
* Add Monaco/VS Code-style sticky scroll to a CodeMirror 6 editor.
|
|
9
7
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { stickyScrollFacet } from "./facet";
|
|
2
2
|
import { scrollStickyPlugin } from "./plugin";
|
|
3
3
|
import { stickyScrollBaseTheme } from "./theme";
|
|
4
|
-
export {
|
|
5
|
-
export { stickyScrollBaseTheme } from "./theme";
|
|
4
|
+
export { defaultExcludeNode } from "./facet";
|
|
6
5
|
/**
|
|
7
6
|
* Add Monaco/VS Code-style sticky scroll to a CodeMirror 6 editor.
|
|
8
7
|
*
|
package/dist/render.js
CHANGED
|
@@ -16,9 +16,6 @@ class StateHighlighter {
|
|
|
16
16
|
style(tags) {
|
|
17
17
|
return highlightingFor(this.state, tags, this.topNodeType);
|
|
18
18
|
}
|
|
19
|
-
scope() {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
19
|
}
|
|
23
20
|
// ---------------------------------------------------------------------------
|
|
24
21
|
// Internal: find the live `.cm-line` DOM element for a document line
|
|
@@ -35,13 +32,7 @@ function findRenderedLineElement(view, lineNumber) {
|
|
|
35
32
|
// (Block widgets, placeholder lines and render-margin lines make those two
|
|
36
33
|
// lists diverge, which previously caused wrong — blank / comment / `}` —
|
|
37
34
|
// line text to be cloned into the sticky bar.)
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
dom = view.domAtPos(lineFrom, 1);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
35
|
+
const dom = view.domAtPos(lineFrom, 1);
|
|
45
36
|
let el = dom.node instanceof HTMLElement ? dom.node : dom.node.parentElement;
|
|
46
37
|
while (el && !el.classList.contains("cm-line"))
|
|
47
38
|
el = el.parentElement;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bhsd/codemirror-stickyscroll",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "VS Code / Monaco-style sticky scroll (sticky lines) extension for CodeMirror 6",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codemirror",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"@codemirror/view": "^6.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@bhsd/code-standard": "^4.
|
|
43
|
+
"@bhsd/code-standard": "^4.3.0",
|
|
44
44
|
"@codemirror/language": "^6.12.4",
|
|
45
|
-
"@codemirror/state": "^6.7.
|
|
46
|
-
"@codemirror/view": "^6.43.
|
|
47
|
-
"eslint": "^10.
|
|
48
|
-
"markdownlint-cli2": "^0.23.
|
|
49
|
-
"stylelint": "^17.
|
|
45
|
+
"@codemirror/state": "^6.7.6",
|
|
46
|
+
"@codemirror/view": "^6.43.13",
|
|
47
|
+
"eslint": "^10.11.0",
|
|
48
|
+
"markdownlint-cli2": "^0.23.3",
|
|
49
|
+
"stylelint": "^17.16.0",
|
|
50
50
|
"typescript": "^6.0.3"
|
|
51
51
|
}
|
|
52
52
|
}
|