@dxos/ui-editor 0.8.4-main.abd8ff62ef → 0.8.4-main.bc2380dfbc
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 +102 -5
- package/README.md +1 -1
- package/dist/lib/browser/index.mjs +503 -469
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +503 -469
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts +5 -2
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -3
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
- package/dist/types/src/extensions/{scroller.d.ts → scrolling/crawler.d.ts} +13 -6
- package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/index.d.ts +5 -0
- package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
- package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -32
- package/src/extensions/autocomplete/placeholder.ts +37 -18
- package/src/extensions/automerge/automerge.test.tsx +8 -2
- package/src/extensions/factories.ts +1 -1
- package/src/extensions/index.ts +1 -3
- package/src/extensions/outliner/outliner.ts +1 -1
- package/src/extensions/{auto-scroll.ts → scrolling/auto-scroll.ts} +37 -25
- package/src/extensions/{scroller.ts → scrolling/crawler.ts} +20 -13
- package/src/extensions/scrolling/index.ts +8 -0
- package/src/extensions/{scroll-past-end.ts → scrolling/scroll-past-end.ts} +6 -6
- package/src/extensions/scrolling/scroller.ts +27 -0
- package/src/extensions/tags/xml-formatting.ts +1 -1
- package/src/extensions/tags/xml-tags.ts +4 -4
- package/src/styles/theme.ts +8 -7
- package/dist/types/src/extensions/auto-scroll.d.ts.map +0 -1
- package/dist/types/src/extensions/scroll-past-end.d.ts.map +0 -1
- package/dist/types/src/extensions/scroller.d.ts.map +0 -1
- /package/dist/types/src/extensions/{auto-scroll.d.ts → scrolling/auto-scroll.d.ts} +0 -0
- /package/dist/types/src/extensions/{scroll-past-end.d.ts → scrolling/scroll-past-end.d.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/ui-editor",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.bc2380dfbc",
|
|
4
4
|
"description": "Text editor components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"src"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@automerge/automerge": "3.2.
|
|
35
|
+
"@automerge/automerge": "3.2.6",
|
|
36
36
|
"@codemirror/autocomplete": "^6.19.0",
|
|
37
37
|
"@codemirror/commands": "^6.8.1",
|
|
38
38
|
"@codemirror/lang-html": "^6.4.11",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@codemirror/search": "^6.5.11",
|
|
48
48
|
"@codemirror/state": "^6.5.2",
|
|
49
49
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
50
|
-
"@codemirror/view": "^6.38.
|
|
50
|
+
"@codemirror/view": "^6.38.5",
|
|
51
51
|
"@lezer/common": "^1.2.2",
|
|
52
52
|
"@lezer/generator": "^1.7.1",
|
|
53
53
|
"@lezer/highlight": "^1.2.1",
|
|
@@ -62,27 +62,27 @@
|
|
|
62
62
|
"lodash.merge": "^4.6.2",
|
|
63
63
|
"lodash.sortby": "^4.7.0",
|
|
64
64
|
"style-mod": "^4.1.0",
|
|
65
|
-
"@dxos/app-graph": "0.8.4-main.
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/
|
|
72
|
-
"@dxos/
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/
|
|
75
|
-
"@dxos/
|
|
76
|
-
"@dxos/protocols": "0.8.4-main.
|
|
77
|
-
"@dxos/ui": "0.8.4-main.
|
|
78
|
-
"@dxos/
|
|
79
|
-
"@dxos/ui-types": "0.8.4-main.
|
|
80
|
-
"@dxos/
|
|
65
|
+
"@dxos/app-graph": "0.8.4-main.bc2380dfbc",
|
|
66
|
+
"@dxos/async": "0.8.4-main.bc2380dfbc",
|
|
67
|
+
"@dxos/client": "0.8.4-main.bc2380dfbc",
|
|
68
|
+
"@dxos/debug": "0.8.4-main.bc2380dfbc",
|
|
69
|
+
"@dxos/context": "0.8.4-main.bc2380dfbc",
|
|
70
|
+
"@dxos/echo": "0.8.4-main.bc2380dfbc",
|
|
71
|
+
"@dxos/display-name": "0.8.4-main.bc2380dfbc",
|
|
72
|
+
"@dxos/invariant": "0.8.4-main.bc2380dfbc",
|
|
73
|
+
"@dxos/echo-db": "0.8.4-main.bc2380dfbc",
|
|
74
|
+
"@dxos/log": "0.8.4-main.bc2380dfbc",
|
|
75
|
+
"@dxos/lit-ui": "0.8.4-main.bc2380dfbc",
|
|
76
|
+
"@dxos/protocols": "0.8.4-main.bc2380dfbc",
|
|
77
|
+
"@dxos/ui-theme": "0.8.4-main.bc2380dfbc",
|
|
78
|
+
"@dxos/ui": "0.8.4-main.bc2380dfbc",
|
|
79
|
+
"@dxos/ui-types": "0.8.4-main.bc2380dfbc",
|
|
80
|
+
"@dxos/util": "0.8.4-main.bc2380dfbc"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@automerge/automerge": "3.2.
|
|
84
|
-
"@automerge/automerge-repo": "2.
|
|
85
|
-
"@automerge/automerge-repo-network-broadcastchannel": "2.
|
|
83
|
+
"@automerge/automerge": "3.2.6",
|
|
84
|
+
"@automerge/automerge-repo": "2.6.0-subduction.17",
|
|
85
|
+
"@automerge/automerge-repo-network-broadcastchannel": "2.6.0-subduction.17",
|
|
86
86
|
"@effect/platform": "0.94.4",
|
|
87
87
|
"@types/chai": "^4.2.15",
|
|
88
88
|
"@types/chai-dom": "^1.11.0",
|
|
@@ -95,21 +95,21 @@
|
|
|
95
95
|
"happy-dom": "^20.0.0",
|
|
96
96
|
"jsdom": "^27.0.0",
|
|
97
97
|
"mocha": "^10.6.0",
|
|
98
|
-
"vite": "^8.0.
|
|
98
|
+
"vite": "^8.0.13",
|
|
99
99
|
"vite-plugin-top-level-await": "^1.6.0",
|
|
100
100
|
"vite-plugin-wasm": "^3.6.0",
|
|
101
|
-
"@dxos/
|
|
102
|
-
"@dxos/
|
|
103
|
-
"@dxos/
|
|
104
|
-
"@dxos/
|
|
105
|
-
"@dxos/
|
|
106
|
-
"@dxos/
|
|
107
|
-
"@dxos/
|
|
101
|
+
"@dxos/echo": "0.8.4-main.bc2380dfbc",
|
|
102
|
+
"@dxos/config": "0.8.4-main.bc2380dfbc",
|
|
103
|
+
"@dxos/random": "0.8.4-main.bc2380dfbc",
|
|
104
|
+
"@dxos/schema": "0.8.4-main.bc2380dfbc",
|
|
105
|
+
"@dxos/ui-theme": "0.8.4-main.bc2380dfbc",
|
|
106
|
+
"@dxos/keyboard": "0.8.4-main.bc2380dfbc",
|
|
107
|
+
"@dxos/storybook-utils": "0.8.4-main.bc2380dfbc"
|
|
108
108
|
},
|
|
109
109
|
"peerDependencies": {
|
|
110
110
|
"@effect/platform": "0.94.4",
|
|
111
111
|
"effect": "3.20.0",
|
|
112
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
112
|
+
"@dxos/ui-theme": "0.8.4-main.bc2380dfbc"
|
|
113
113
|
},
|
|
114
114
|
"publishConfig": {
|
|
115
115
|
"access": "public"
|
|
@@ -13,42 +13,61 @@ type Content = string | HTMLElement | ((view: EditorView) => HTMLElement);
|
|
|
13
13
|
export type PlaceholderOptions = {
|
|
14
14
|
content: Content;
|
|
15
15
|
delay?: number;
|
|
16
|
+
focusOnly?: boolean;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* Shows a transient placeholder at the current cursor position.
|
|
20
|
+
* Shows a transient placeholder at the current cursor position. When
|
|
21
|
+
* `focusOnly` is set the placeholder is suppressed unless the editor has DOM
|
|
22
|
+
* focus, and is hidden again when focus is lost.
|
|
20
23
|
*/
|
|
21
|
-
export const placeholder = ({ content, delay = 3_000 }: PlaceholderOptions): Extension => {
|
|
24
|
+
export const placeholder = ({ content, delay = 3_000, focusOnly = false }: PlaceholderOptions): Extension => {
|
|
22
25
|
const plugin = ViewPlugin.fromClass(
|
|
23
26
|
class {
|
|
24
27
|
_timeout: ReturnType<typeof setTimeout> | undefined;
|
|
25
28
|
_decorations = Decoration.none;
|
|
26
29
|
|
|
27
30
|
update(update: ViewUpdate) {
|
|
31
|
+
// React to actual user activity only. The empty `view.update([])`
|
|
32
|
+
// dispatched from the timeout below carries no doc/selection/focus
|
|
33
|
+
// change, so it falls through here as a no-op — that's how the
|
|
34
|
+
// freshly-set widget survives long enough for the decoration
|
|
35
|
+
// provider to read it. Without this gate, the unconditional reset
|
|
36
|
+
// would clobber the decoration in the same tick and the placeholder
|
|
37
|
+
// would never visibly render.
|
|
38
|
+
if (!update.docChanged && !update.selectionSet && !update.focusChanged) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
if (this._timeout) {
|
|
29
43
|
window.clearTimeout(this._timeout);
|
|
30
44
|
this._timeout = undefined;
|
|
31
45
|
}
|
|
46
|
+
this._decorations = Decoration.none;
|
|
47
|
+
|
|
48
|
+
// Honour `focusOnly`: when the option is set and the editor isn't
|
|
49
|
+
// focused, leave the placeholder hidden and skip rescheduling. The
|
|
50
|
+
// next `focusChanged` update reschedules once focus returns.
|
|
51
|
+
if (focusOnly && !update.view.hasFocus) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
32
54
|
|
|
33
|
-
// Check if the active line (where cursor is) is empty.
|
|
34
55
|
const activeLine = update.view.state.doc.lineAt(update.view.state.selection.main.head);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Create widget decoration at the start of the current line.
|
|
38
|
-
const lineStart = activeLine.from;
|
|
39
|
-
this._timeout = setTimeout(() => {
|
|
40
|
-
this._decorations = Decoration.set([
|
|
41
|
-
Decoration.widget({
|
|
42
|
-
widget: new PlaceholderWidget(content),
|
|
43
|
-
side: 1,
|
|
44
|
-
}).range(lineStart),
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
update.view.update([]);
|
|
48
|
-
}, delay);
|
|
56
|
+
if (activeLine.text.trim() !== '') {
|
|
57
|
+
return;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
|
|
60
|
+
const lineStart = activeLine.from;
|
|
61
|
+
const view = update.view;
|
|
62
|
+
this._timeout = setTimeout(() => {
|
|
63
|
+
this._decorations = Decoration.set([
|
|
64
|
+
Decoration.widget({
|
|
65
|
+
widget: new PlaceholderWidget(content),
|
|
66
|
+
side: 1,
|
|
67
|
+
}).range(lineStart),
|
|
68
|
+
]);
|
|
69
|
+
view.update([]);
|
|
70
|
+
}, delay);
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
destroy() {
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type ChangeFn, type ChangeOptions, type Doc, type Heads } from '@automerge/automerge';
|
|
6
|
-
import { type DocHandle, Repo, decodeHeads, encodeHeads } from '@automerge/automerge-repo';
|
|
6
|
+
import { type DocHandle, Repo, decodeHeads, encodeHeads, initSubduction } from '@automerge/automerge-repo';
|
|
7
7
|
import { EditorState } from '@codemirror/state';
|
|
8
8
|
import { EditorView } from '@codemirror/view';
|
|
9
9
|
import { render, screen } from '@testing-library/react';
|
|
10
10
|
import React, { type FC, useEffect, useRef, useState } from 'react';
|
|
11
|
-
import { describe, test } from 'vitest';
|
|
11
|
+
import { beforeAll, describe, test } from 'vitest';
|
|
12
12
|
|
|
13
13
|
import { type IDocHandle } from '@dxos/echo-db';
|
|
14
14
|
import { getDeep } from '@dxos/util';
|
|
@@ -81,6 +81,12 @@ const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ han
|
|
|
81
81
|
// TODO(burdon): https://testing-library.com/docs/react-testing-library/example-intro/
|
|
82
82
|
|
|
83
83
|
describe('Automerge', () => {
|
|
84
|
+
// Subduction-fork `Repo` constructs a `MemorySigner` internally; WASM must be
|
|
85
|
+
// initialized first or the constructor throws `'set_subduction_logger' of undefined`.
|
|
86
|
+
beforeAll(async () => {
|
|
87
|
+
await initSubduction();
|
|
88
|
+
});
|
|
89
|
+
|
|
84
90
|
test('basic sync', ({ expect }) => {
|
|
85
91
|
const repo = new Repo({ network: [] });
|
|
86
92
|
const handle = repo.create<TestObject>();
|
|
@@ -33,7 +33,7 @@ import { baseTheme, createFontTheme, editorGutter } from '../styles';
|
|
|
33
33
|
import { automerge } from './automerge';
|
|
34
34
|
import { SpaceAwarenessProvider, awareness } from './awareness';
|
|
35
35
|
import { focus } from './focus';
|
|
36
|
-
import { scrollPastEnd } from './
|
|
36
|
+
import { scrollPastEnd } from './scrolling';
|
|
37
37
|
|
|
38
38
|
//
|
|
39
39
|
// Basic
|
package/src/extensions/index.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
export * from './annotations';
|
|
6
6
|
export * from './autocomplete';
|
|
7
|
-
export * from './auto-scroll';
|
|
8
7
|
export * from './automerge';
|
|
9
8
|
export * from './awareness';
|
|
10
9
|
export * from './blast';
|
|
@@ -26,8 +25,7 @@ export * from './modes';
|
|
|
26
25
|
export * from './outliner';
|
|
27
26
|
export * from './preview';
|
|
28
27
|
export * from './replacer';
|
|
29
|
-
export * from './
|
|
30
|
-
export * from './scroller';
|
|
28
|
+
export * from './scrolling';
|
|
31
29
|
export * from './selection';
|
|
32
30
|
export * from './snippets';
|
|
33
31
|
export * from './state';
|
|
@@ -156,7 +156,7 @@ const decorations = () => [
|
|
|
156
156
|
},
|
|
157
157
|
|
|
158
158
|
'.cm-list-item-focused': {
|
|
159
|
-
borderColor: 'var(--color-
|
|
159
|
+
borderColor: 'var(--color-focus-ring-subtle)',
|
|
160
160
|
},
|
|
161
161
|
'&:focus-within .cm-list-item-selected': {
|
|
162
162
|
borderColor: 'var(--color-separator)',
|
|
@@ -9,7 +9,7 @@ import { addEventListener, combine, throttle } from '@dxos/async';
|
|
|
9
9
|
import { Domino } from '@dxos/ui';
|
|
10
10
|
import { getSize } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { crawlerActiveEffect, crawlerLineEffect } from './crawler';
|
|
13
13
|
|
|
14
14
|
/** Enable or disable autoscroll. */
|
|
15
15
|
export const autoScrollEffect = StateEffect.define<boolean>();
|
|
@@ -54,11 +54,11 @@ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
|
54
54
|
if (enabled) {
|
|
55
55
|
setPinned(true);
|
|
56
56
|
view.dispatch({
|
|
57
|
-
effects:
|
|
57
|
+
effects: crawlerActiveEffect.of(true),
|
|
58
58
|
});
|
|
59
59
|
} else {
|
|
60
60
|
view.dispatch({
|
|
61
|
-
effects:
|
|
61
|
+
effects: crawlerActiveEffect.of(false),
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -96,9 +96,7 @@ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
|
96
96
|
const delta = scrollHeight - scrollTop - clientHeight;
|
|
97
97
|
if (delta > 0) {
|
|
98
98
|
setPinned(true);
|
|
99
|
-
view.dispatch({
|
|
100
|
-
effects: scrollerCrawlEffect.of(true),
|
|
101
|
-
});
|
|
99
|
+
view.dispatch({ effects: crawlerActiveEffect.of(true) });
|
|
102
100
|
} else if (delta < -1) {
|
|
103
101
|
setPinned(false);
|
|
104
102
|
}
|
|
@@ -122,20 +120,23 @@ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
|
122
120
|
private destroyed = false;
|
|
123
121
|
constructor(view: EditorView) {
|
|
124
122
|
// Throttle so a continuous drag-resize (or a flurry of layout changes) coalesces
|
|
125
|
-
// into a single re-pin per ~
|
|
123
|
+
// into a single re-pin per ~50ms instead of dispatching every frame.
|
|
126
124
|
const onResize = throttle(() => {
|
|
127
125
|
if (this.destroyed || !enabled) {
|
|
128
126
|
return;
|
|
129
127
|
}
|
|
128
|
+
|
|
130
129
|
setPinned(true);
|
|
131
130
|
requestAnimationFrame(() => {
|
|
132
131
|
if (this.destroyed) {
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
|
-
|
|
136
|
-
view.
|
|
134
|
+
|
|
135
|
+
view.scrollDOM.scrollTo({ top: view.scrollDOM.scrollHeight, behavior: 'instant' });
|
|
136
|
+
view.dispatch({ effects: crawlerActiveEffect.of(false) });
|
|
137
137
|
});
|
|
138
|
-
},
|
|
138
|
+
}, 50);
|
|
139
|
+
|
|
139
140
|
this.observer = new ResizeObserver(() => {
|
|
140
141
|
// Skip the initial fire that ResizeObserver emits on `observe()`.
|
|
141
142
|
if (this.firstObservation) {
|
|
@@ -144,6 +145,7 @@ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
|
144
145
|
}
|
|
145
146
|
onResize();
|
|
146
147
|
});
|
|
148
|
+
|
|
147
149
|
this.observer.observe(view.scrollDOM);
|
|
148
150
|
}
|
|
149
151
|
destroy() {
|
|
@@ -159,20 +161,30 @@ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
|
159
161
|
class {
|
|
160
162
|
private readonly cleanup: () => void;
|
|
161
163
|
constructor(view: EditorView) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
// Re-pin check is throttled so the listener doesn't thrash while scrolling, but
|
|
165
|
+
// unpinning must be immediate — otherwise content arriving during the throttle
|
|
166
|
+
// window re-applies the crawl effect and yanks the viewport back to the bottom.
|
|
167
|
+
const onUserScroll = throttle(() => {
|
|
168
|
+
requestAnimationFrame(() => {
|
|
169
|
+
const { scrollTop, scrollHeight, clientHeight } = view.scrollDOM;
|
|
170
|
+
const delta = scrollHeight - scrollTop - clientHeight;
|
|
171
|
+
// Sub-pixel tolerance: fractional scroll positions can leave delta at e.g. 0.5
|
|
172
|
+
// even when the user is visually at the bottom.
|
|
173
|
+
const pinned = Math.abs(delta) <= 1;
|
|
174
|
+
setPinned(pinned);
|
|
175
|
+
if (!pinned) {
|
|
176
|
+
view.dispatch({ effects: crawlerActiveEffect.of(false) });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}, 500);
|
|
180
|
+
|
|
181
|
+
this.cleanup = createUserScrollDetector(view.scrollDOM, () => {
|
|
182
|
+
if (isPinned) {
|
|
183
|
+
setPinned(false);
|
|
184
|
+
view.dispatch({ effects: crawlerActiveEffect.of(false) });
|
|
185
|
+
}
|
|
186
|
+
onUserScroll();
|
|
187
|
+
});
|
|
176
188
|
}
|
|
177
189
|
destroy() {
|
|
178
190
|
this.cleanup();
|
|
@@ -194,7 +206,7 @@ export const autoScroll = ({ scrollOnResize = true }: AutoScrollProps = {}) => {
|
|
|
194
206
|
.on('click', () => {
|
|
195
207
|
setPinned(true);
|
|
196
208
|
view.dispatch({
|
|
197
|
-
effects:
|
|
209
|
+
effects: crawlerLineEffect.of({ line: -1, position: 'end', behavior: 'smooth' }),
|
|
198
210
|
});
|
|
199
211
|
});
|
|
200
212
|
|
|
@@ -40,10 +40,10 @@ export type ScrollToProps = {
|
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/** Scroll to a specific line. */
|
|
43
|
-
export const
|
|
43
|
+
export const crawlerLineEffect = StateEffect.define<ScrollToProps>();
|
|
44
44
|
|
|
45
45
|
/** Start/stop crawling the end of the document. */
|
|
46
|
-
export const
|
|
46
|
+
export const crawlerActiveEffect = StateEffect.define<boolean>();
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Helper function to scroll to a specific line.
|
|
@@ -51,22 +51,29 @@ export const scrollerCrawlEffect = StateEffect.define<boolean>();
|
|
|
51
51
|
*/
|
|
52
52
|
export const scrollToLine = (view: EditorView, options: ScrollToProps) => {
|
|
53
53
|
view.dispatch({
|
|
54
|
-
effects:
|
|
54
|
+
effects: crawlerLineEffect.of(options),
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
export type
|
|
58
|
+
export type CrawlerOptions = {
|
|
59
59
|
/** Threshold in px to trigger scroll from bottom. */
|
|
60
60
|
overScroll?: number;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* Imperative scroll-control primitive for streaming editor views.
|
|
65
|
+
*
|
|
66
|
+
* Owns the scroll-related effects (`crawlerLineEffect`, `crawlerActiveEffect`), the spring
|
|
67
|
+
* crawler that follows the bottom of the document, and the `.cm-scroller` theme (overflow,
|
|
68
|
+
* scrollbar styling, and the `::after` overscroll spacer).
|
|
69
|
+
*
|
|
70
|
+
* Use directly for jump-to-line navigation, or pair with `autoScroll` for a pin-to-bottom
|
|
71
|
+
* streaming policy. The composite `streamScroll` bundles both for the common case.
|
|
65
72
|
*/
|
|
66
|
-
export const
|
|
73
|
+
export const crawler = ({ overScroll = 0 }: CrawlerOptions = {}) => {
|
|
67
74
|
// ViewPlugin to manage scroll animations.
|
|
68
|
-
const
|
|
69
|
-
class
|
|
75
|
+
const crawlerPlugin = ViewPlugin.fromClass(
|
|
76
|
+
class CrawlerPlugin {
|
|
70
77
|
private readonly crawler: ReturnType<typeof createCrawler>;
|
|
71
78
|
constructor(private readonly view: EditorView) {
|
|
72
79
|
this.crawler = createCrawler(this.view);
|
|
@@ -126,18 +133,18 @@ export const scroller = ({ overScroll = 0 }: ScrollerOptions = {}) => {
|
|
|
126
133
|
);
|
|
127
134
|
|
|
128
135
|
return [
|
|
129
|
-
|
|
136
|
+
crawlerPlugin,
|
|
130
137
|
|
|
131
|
-
// Listen for effect.
|
|
138
|
+
// Listen for effect.
|
|
132
139
|
EditorView.updateListener.of((update) => {
|
|
133
140
|
update.transactions.forEach((transaction) => {
|
|
134
141
|
try {
|
|
135
|
-
const plugin = update.view.plugin(
|
|
142
|
+
const plugin = update.view.plugin(crawlerPlugin);
|
|
136
143
|
if (plugin) {
|
|
137
144
|
for (const effect of transaction.effects) {
|
|
138
|
-
if (effect.is(
|
|
145
|
+
if (effect.is(crawlerActiveEffect)) {
|
|
139
146
|
plugin.crawl(effect.value);
|
|
140
|
-
} else if (effect.is(
|
|
147
|
+
} else if (effect.is(crawlerLineEffect)) {
|
|
141
148
|
plugin.scroll(effect.value);
|
|
142
149
|
}
|
|
143
150
|
}
|
|
@@ -12,15 +12,15 @@ import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
|
12
12
|
*/
|
|
13
13
|
const scrollPastEndPlugin = ViewPlugin.fromClass(
|
|
14
14
|
class {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
_height = 1_000;
|
|
16
|
+
_attrs: { style: string } | null = { style: `padding-bottom: ${this._height}px` };
|
|
17
17
|
|
|
18
18
|
update({ view }: { view: EditorView }) {
|
|
19
19
|
const lastLineBlock = view.lineBlockAt(view.state.doc.length);
|
|
20
20
|
const height = view.dom.clientHeight - lastLineBlock.height - view.documentPadding.top - 0.5;
|
|
21
|
-
if (height >= 0 && height !== this.
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
21
|
+
if (height >= 0 && height !== this._height) {
|
|
22
|
+
this._height = height;
|
|
23
|
+
this._attrs = { style: `padding-bottom: ${height}px` };
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -28,5 +28,5 @@ const scrollPastEndPlugin = ViewPlugin.fromClass(
|
|
|
28
28
|
|
|
29
29
|
export const scrollPastEnd = (): Extension => [
|
|
30
30
|
scrollPastEndPlugin,
|
|
31
|
-
EditorView.contentAttributes.of((view) => view.plugin(scrollPastEndPlugin)?.
|
|
31
|
+
EditorView.contentAttributes.of((view) => view.plugin(scrollPastEndPlugin)?._attrs ?? null),
|
|
32
32
|
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import { isTruthy } from '@dxos/util';
|
|
8
|
+
|
|
9
|
+
import { type AutoScrollProps, autoScroll as autoScrollExtension } from './auto-scroll';
|
|
10
|
+
import { type CrawlerOptions, crawler } from './crawler';
|
|
11
|
+
|
|
12
|
+
export type ScrollerOptions = CrawlerOptions &
|
|
13
|
+
AutoScrollProps & {
|
|
14
|
+
/**
|
|
15
|
+
* Include the auto-scroll policy (pin-to-bottom, user-scroll unpin, scroll-to-bottom button).
|
|
16
|
+
* Set to `false` to get only the crawler primitive (line jumps, theme, overscroll spacer).
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
autoScroll?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Composite scroll extension for streaming editor views (chat threads, transcripts, logs).
|
|
24
|
+
*/
|
|
25
|
+
export const scroller = ({ overScroll, scrollOnResize, autoScroll = true }: ScrollerOptions = {}): Extension[] => {
|
|
26
|
+
return [crawler({ overScroll }), autoScroll && autoScrollExtension({ scrollOnResize })].filter(isTruthy);
|
|
27
|
+
};
|
|
@@ -111,7 +111,7 @@ export const xmlFormatting = ({ skip }: XmlFormattingOptions = {}): Extension =>
|
|
|
111
111
|
|
|
112
112
|
EditorView.baseTheme({
|
|
113
113
|
'.cm-xml-element': {
|
|
114
|
-
backgroundColor: 'var(--color-
|
|
114
|
+
backgroundColor: 'var(--color-current-surface)',
|
|
115
115
|
borderRadius: '0.25rem',
|
|
116
116
|
padding: '0.25rem',
|
|
117
117
|
},
|
|
@@ -21,7 +21,7 @@ import { Domino } from '@dxos/ui';
|
|
|
21
21
|
|
|
22
22
|
import { type Range } from '../../types';
|
|
23
23
|
import { decorationSetToArray } from '../../util';
|
|
24
|
-
import {
|
|
24
|
+
import { crawlerLineEffect } from '../scrolling';
|
|
25
25
|
import { nodeToJson } from './xml-util';
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -299,7 +299,7 @@ const createNavigationEffectPlugin = (
|
|
|
299
299
|
const line = view.state.doc.lineAt(widget?.from ?? 0);
|
|
300
300
|
view.dispatch({
|
|
301
301
|
selection: { anchor: line.from, head: line.from },
|
|
302
|
-
effects:
|
|
302
|
+
effects: crawlerLineEffect.of({ line: line.number - 1, offset: -16 }),
|
|
303
303
|
});
|
|
304
304
|
|
|
305
305
|
continue;
|
|
@@ -325,13 +325,13 @@ const createNavigationEffectPlugin = (
|
|
|
325
325
|
const line = view.state.doc.lineAt(widget?.from);
|
|
326
326
|
view.dispatch({
|
|
327
327
|
selection: { anchor: line.to, head: line.to },
|
|
328
|
-
effects:
|
|
328
|
+
effects: crawlerLineEffect.of({ line: line.number - 1, offset: -16 }),
|
|
329
329
|
});
|
|
330
330
|
} else {
|
|
331
331
|
const line = view.state.doc.lineAt(view.state.doc.length);
|
|
332
332
|
view.dispatch({
|
|
333
333
|
selection: { anchor: line.to, head: line.to },
|
|
334
|
-
effects:
|
|
334
|
+
effects: crawlerLineEffect.of({ line: line.number - 1, position: 'end' }),
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
337
|
|
package/src/styles/theme.ts
CHANGED
|
@@ -108,7 +108,7 @@ export const baseTheme = EditorView.baseTheme({
|
|
|
108
108
|
* Scroller
|
|
109
109
|
*/
|
|
110
110
|
'.cm-scroller': {
|
|
111
|
-
// Browser scroll-anchoring: see comment in `
|
|
111
|
+
// Browser scroll-anchoring: see comment in `scrolling/crawler.ts`. `auto` lets the browser pin a
|
|
112
112
|
// stable element near the viewport top so widget resizes (e.g. tool-block TogglePanel
|
|
113
113
|
// open/close) don't jump the user's view.
|
|
114
114
|
overflowAnchor: 'auto',
|
|
@@ -227,6 +227,7 @@ export const baseTheme = EditorView.baseTheme({
|
|
|
227
227
|
textDecorationColor: 'var(--color-separator)',
|
|
228
228
|
textUnderlineOffset: '2px',
|
|
229
229
|
borderRadius: '.125rem',
|
|
230
|
+
cursor: 'pointer',
|
|
230
231
|
},
|
|
231
232
|
'.cm-link > span': {
|
|
232
233
|
color: 'var(--color-accent-text)',
|
|
@@ -266,12 +267,12 @@ export const baseTheme = EditorView.baseTheme({
|
|
|
266
267
|
padding: '4px',
|
|
267
268
|
},
|
|
268
269
|
'.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
|
269
|
-
background: 'var(--color-
|
|
270
|
-
color: 'var(--color-base-
|
|
270
|
+
background: 'var(--color-current-surface)',
|
|
271
|
+
color: 'var(--color-base-foreground)',
|
|
271
272
|
},
|
|
272
273
|
'.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
|
|
273
274
|
paddingLeft: '4px !important',
|
|
274
|
-
color: 'var(--color-base-
|
|
275
|
+
color: 'var(--color-base-foreground)',
|
|
275
276
|
},
|
|
276
277
|
|
|
277
278
|
/**
|
|
@@ -291,7 +292,7 @@ export const baseTheme = EditorView.baseTheme({
|
|
|
291
292
|
padding: '0 4px',
|
|
292
293
|
},
|
|
293
294
|
'.cm-completionMatchedText': {
|
|
294
|
-
color: 'var(--color-base-
|
|
295
|
+
color: 'var(--color-base-foreground)',
|
|
295
296
|
textDecoration: 'none !important',
|
|
296
297
|
},
|
|
297
298
|
|
|
@@ -327,7 +328,7 @@ export const baseTheme = EditorView.baseTheme({
|
|
|
327
328
|
backgroundColor: 'var(--color-input-surface)',
|
|
328
329
|
},
|
|
329
330
|
'.cm-panel input:focus, .cm-panel button:focus': {
|
|
330
|
-
outline: '1px solid var(--color-
|
|
331
|
+
outline: '1px solid var(--color-focus-ring-subtle)',
|
|
331
332
|
},
|
|
332
333
|
'.cm-panel label': {
|
|
333
334
|
display: 'inline-flex',
|
|
@@ -340,7 +341,7 @@ export const baseTheme = EditorView.baseTheme({
|
|
|
340
341
|
height: '8px',
|
|
341
342
|
marginRight: '6px !important',
|
|
342
343
|
padding: '2px !important',
|
|
343
|
-
color: 'var(--color-
|
|
344
|
+
color: 'var(--color-focus-ring-subtle)',
|
|
344
345
|
},
|
|
345
346
|
'.cm-panel button': {
|
|
346
347
|
'&:hover': {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auto-scroll.d.ts","sourceRoot":"","sources":["../../../../src/extensions/auto-scroll.ts"],"names":[],"mappings":"AAaA,oCAAoC;AACpC,eAAO,MAAM,gBAAgB,sDAAgC,CAAC;AAE9D,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,UAAU,wBAA+B,eAAe,4CAkLpE,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-past-end.d.ts","sourceRoot":"","sources":["../../../../src/extensions/scroll-past-end.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAwBnD,eAAO,MAAM,aAAa,QAAO,SAGhC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scroller.d.ts","sourceRoot":"","sources":["../../../../src/extensions/scroller.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAI1D;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAE3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,CAAC;AAEF,iCAAiC;AACjC,eAAO,MAAM,kBAAkB,4DAAsC,CAAC;AAEtE,mDAAmD;AACnD,eAAO,MAAM,mBAAmB,sDAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,YAAY,SAAU,UAAU,WAAW,aAAa,SAIpE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,QAAQ,oBAAwB,eAAe,4CA0H3D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,SAAI,EAAE,aAAa,SAAI,EAAE,YAAY,SAAK;IAiC3F,MAAM;IAON,MAAM;EAST"}
|
|
File without changes
|
|
File without changes
|