@dxos/react-ui-editor 0.8.4-main.406dc2a → 0.8.4-main.548089c
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/dist/lib/browser/index.mjs +1379 -1139
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1379 -1139
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Editor/Editor.stories.d.ts +0 -3
- package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +17 -2
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/util.d.ts +5 -19
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/autoscroll.d.ts +14 -4
- package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/blocks.d.ts +2 -0
- package/dist/types/src/extensions/blocks.d.ts.map +1 -0
- package/dist/types/src/extensions/bookmarks.d.ts +12 -0
- package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +4 -4
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +4 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/listener.d.ts +8 -6
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +1 -2
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts +1 -1
- package/dist/types/src/extensions/popover/PopoverMenuProvider.d.ts.map +1 -1
- package/dist/types/src/extensions/popover/popover.d.ts.map +1 -1
- package/dist/types/src/extensions/preview/preview.d.ts +6 -2
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
- package/dist/types/src/extensions/replacer.d.ts +21 -0
- package/dist/types/src/extensions/replacer.d.ts.map +1 -0
- package/dist/types/src/extensions/replacer.test.d.ts +2 -0
- package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling.d.ts +78 -0
- package/dist/types/src/extensions/scrolling.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +41 -16
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Popover.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
- package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
- package/dist/types/src/stories/components/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -38
- package/src/components/Editor/Editor.stories.tsx +4 -7
- package/src/components/EditorToolbar/EditorToolbar.tsx +90 -90
- package/src/components/EditorToolbar/headings.ts +6 -4
- package/src/components/EditorToolbar/util.ts +4 -20
- package/src/extensions/autocomplete/autocomplete.ts +5 -5
- package/src/extensions/automerge/automerge.stories.tsx +1 -1
- package/src/extensions/automerge/automerge.ts +1 -1
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/sync.ts +1 -1
- package/src/extensions/automerge/update-automerge.ts +1 -1
- package/src/extensions/autoscroll.ts +74 -68
- package/src/extensions/awareness/awareness-provider.ts +2 -2
- package/src/extensions/blocks.ts +131 -0
- package/src/extensions/bookmarks.ts +75 -0
- package/src/extensions/comments.ts +2 -1
- package/src/extensions/factories.ts +6 -4
- package/src/extensions/folding.tsx +1 -2
- package/src/extensions/index.ts +4 -0
- package/src/extensions/listener.ts +14 -20
- package/src/extensions/markdown/bundle.ts +12 -2
- package/src/extensions/markdown/decorate.ts +8 -8
- package/src/extensions/markdown/formatting.ts +8 -8
- package/src/extensions/markdown/highlight.ts +1 -1
- package/src/extensions/markdown/image.ts +2 -2
- package/src/extensions/markdown/table.ts +6 -6
- package/src/extensions/popover/PopoverMenuProvider.tsx +2 -3
- package/src/extensions/popover/popover.ts +0 -4
- package/src/extensions/preview/preview.ts +14 -9
- package/src/extensions/replacer.test.ts +75 -0
- package/src/extensions/replacer.ts +93 -0
- package/src/extensions/scrolling.ts +189 -0
- package/src/extensions/selection.ts +1 -1
- package/src/extensions/tags/extended-markdown.test.ts +2 -1
- package/src/extensions/tags/xml-tags.ts +310 -203
- package/src/extensions/typewriter.ts +1 -1
- package/src/stories/CommandDialog.stories.tsx +9 -4
- package/src/stories/Comments.stories.tsx +1 -1
- package/src/stories/EditorToolbar.stories.tsx +4 -5
- package/src/stories/Popover.stories.tsx +4 -6
- package/src/stories/Preview.stories.tsx +15 -8
- package/src/stories/Tags.stories.tsx +19 -5
- package/src/stories/TextEditor.stories.tsx +2 -2
- package/src/stories/components/EditorStory.tsx +3 -3
- package/src/stories/components/util.tsx +39 -6
- package/src/styles/markdown.ts +1 -1
- package/src/styles/theme.ts +1 -1
|
@@ -5,101 +5,117 @@
|
|
|
5
5
|
import { StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
+
import { debounce } from '@dxos/async';
|
|
8
9
|
import { Domino } from '@dxos/react-ui';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
import { scrollToLineEffect } from './scrolling';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// TODO(burdon): Reconcile with scrollToLineEffect (scrolling).
|
|
14
|
+
export const scrollToBottomEffect = StateEffect.define<ScrollBehavior | undefined>();
|
|
13
15
|
|
|
14
16
|
export type AutoScrollOptions = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/** Auto-scroll when reaches the bottom. */
|
|
18
|
+
autoScroll?: boolean;
|
|
19
|
+
/** Threshold in px to trigger scroll from bottom. */
|
|
20
|
+
threshold?: number;
|
|
21
|
+
/** Throttle time in ms. */
|
|
22
|
+
throttleDelay?: number;
|
|
23
|
+
/** Callback when auto-scrolling. */
|
|
24
|
+
onAutoScroll?: (props: { view: EditorView; distanceFromBottom: number }) => boolean | void;
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
28
|
* Extension that supports pinning the scroll position and automatically scrolls to the bottom when content is added.
|
|
21
29
|
*/
|
|
22
30
|
// TODO(burdon): Reconcile with transcript-extension.
|
|
23
|
-
export const autoScroll = ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
export const autoScroll = ({
|
|
32
|
+
autoScroll = true,
|
|
33
|
+
threshold = 100,
|
|
34
|
+
throttleDelay = 1_000,
|
|
35
|
+
onAutoScroll,
|
|
36
|
+
}: Partial<AutoScrollOptions> = {}) => {
|
|
27
37
|
let buttonContainer: HTMLDivElement | undefined;
|
|
38
|
+
let hideTimeout: NodeJS.Timeout | undefined;
|
|
28
39
|
let lastScrollTop = 0;
|
|
29
|
-
let
|
|
40
|
+
let isPinned = true;
|
|
41
|
+
|
|
42
|
+
const setPinned = (pin: boolean) => {
|
|
43
|
+
isPinned = pin;
|
|
44
|
+
buttonContainer?.classList.toggle('opacity-0', pin);
|
|
45
|
+
};
|
|
30
46
|
|
|
47
|
+
// Temporarily hide the scrollbar while auto-scrolling.
|
|
31
48
|
const hideScrollbar = (view: EditorView) => {
|
|
32
49
|
view.scrollDOM.classList.add('cm-hide-scrollbar');
|
|
33
|
-
clearTimeout(
|
|
34
|
-
|
|
50
|
+
clearTimeout(hideTimeout);
|
|
51
|
+
hideTimeout = setTimeout(() => {
|
|
35
52
|
view.scrollDOM.classList.remove('cm-hide-scrollbar');
|
|
36
53
|
}, 1_000);
|
|
37
54
|
};
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
behavior: 'smooth',
|
|
48
|
-
});
|
|
56
|
+
// Throttled scroll to bottom.
|
|
57
|
+
const scrollToBottom = (view: EditorView, behavior?: ScrollBehavior) => {
|
|
58
|
+
setPinned(true);
|
|
59
|
+
hideScrollbar(view);
|
|
60
|
+
const line = view.state.doc.lineAt(view.state.doc.length);
|
|
61
|
+
view.dispatch({
|
|
62
|
+
selection: { anchor: line.to, head: line.to },
|
|
63
|
+
effects: scrollToLineEffect.of({ line: line.number, options: { position: 'end', offset: threshold, behavior } }),
|
|
49
64
|
});
|
|
50
65
|
};
|
|
51
66
|
|
|
67
|
+
// Throttled check for distance from bottom (for downward scrolls only).
|
|
68
|
+
const checkDistance = debounce((view: EditorView) => {
|
|
69
|
+
const scrollerRect = view.scrollDOM.getBoundingClientRect();
|
|
70
|
+
const coords = view.coordsAtPos(view.state.doc.length);
|
|
71
|
+
const distanceFromBottom = coords ? coords.bottom - scrollerRect.bottom : 0;
|
|
72
|
+
setPinned(distanceFromBottom < 0);
|
|
73
|
+
}, 1_000);
|
|
74
|
+
|
|
75
|
+
// Debounce scroll updates so rapid edits don't cause clunky scrolling.
|
|
76
|
+
const triggerUpdate = debounce((view: EditorView) => scrollToBottom(view), throttleDelay);
|
|
77
|
+
|
|
52
78
|
return [
|
|
53
79
|
// Update listener for logging when scrolling is needed.
|
|
54
|
-
EditorView.updateListener.of((
|
|
55
|
-
//
|
|
56
|
-
|
|
80
|
+
EditorView.updateListener.of(({ view, transactions, heightChanged }) => {
|
|
81
|
+
// TODO(burdon): Remove and use scrollToLineEffect instead.
|
|
82
|
+
transactions.forEach((transaction) => {
|
|
57
83
|
for (const effect of transaction.effects) {
|
|
58
84
|
if (effect.is(scrollToBottomEffect)) {
|
|
59
|
-
scrollToBottom(
|
|
85
|
+
scrollToBottom(view, effect.value);
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
});
|
|
63
89
|
|
|
64
90
|
// Maybe scroll if doc changed and pinned.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}, throttle);
|
|
91
|
+
// NOTE: Geometry changed is triggered when widgets change height (e.g., toggle tool block).
|
|
92
|
+
if (heightChanged && isPinned) {
|
|
93
|
+
const coords = view.coordsAtPos(view.state.doc.length);
|
|
94
|
+
const scrollerRect = view.scrollDOM.getBoundingClientRect();
|
|
95
|
+
const distanceFromBottom = coords ? scrollerRect.bottom - coords.bottom : 0;
|
|
96
|
+
if (autoScroll && distanceFromBottom < threshold) {
|
|
97
|
+
const shouldScroll = onAutoScroll?.({ view, distanceFromBottom }) ?? true;
|
|
98
|
+
if (shouldScroll) {
|
|
99
|
+
triggerUpdate(view);
|
|
100
|
+
}
|
|
101
|
+
} else if (distanceFromBottom < 0) {
|
|
102
|
+
setPinned(false);
|
|
78
103
|
}
|
|
79
104
|
}
|
|
80
105
|
}),
|
|
81
106
|
|
|
82
107
|
// Detect user scroll.
|
|
83
|
-
// NOTE: Multiple scroll events are triggered during programmatic smooth scrolling.
|
|
84
108
|
EditorView.domEventHandlers({
|
|
85
109
|
scroll: (event, view) => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Pin to bottom.
|
|
96
|
-
isPinned = true;
|
|
97
|
-
buttonContainer?.classList.add('opacity-0');
|
|
98
|
-
scrollCounter = 0;
|
|
99
|
-
} else if (scrollCounter > 3) {
|
|
100
|
-
// Break pin if user scrolls up.
|
|
101
|
-
isPinned = false;
|
|
102
|
-
buttonContainer?.classList.remove('opacity-0');
|
|
110
|
+
const currentScrollTop = view.scrollDOM.scrollTop;
|
|
111
|
+
const scrollingUp = currentScrollTop < lastScrollTop;
|
|
112
|
+
lastScrollTop = currentScrollTop;
|
|
113
|
+
|
|
114
|
+
// If user scrolls up, immediately unpin auto-scroll.
|
|
115
|
+
if (scrollingUp) {
|
|
116
|
+
setPinned(false);
|
|
117
|
+
} else {
|
|
118
|
+
checkDistance(view);
|
|
103
119
|
}
|
|
104
120
|
},
|
|
105
121
|
}),
|
|
@@ -108,7 +124,6 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
|
|
|
108
124
|
ViewPlugin.fromClass(
|
|
109
125
|
class {
|
|
110
126
|
constructor(view: EditorView) {
|
|
111
|
-
const scroller = view.scrollDOM.parentElement;
|
|
112
127
|
buttonContainer = Domino.of('div')
|
|
113
128
|
.classNames(true && 'cm-scroll-button transition-opacity duration-300 opacity-0')
|
|
114
129
|
.children(
|
|
@@ -121,7 +136,8 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
|
|
|
121
136
|
}),
|
|
122
137
|
)
|
|
123
138
|
.build();
|
|
124
|
-
|
|
139
|
+
|
|
140
|
+
view.scrollDOM.parentElement!.appendChild(buttonContainer);
|
|
125
141
|
}
|
|
126
142
|
},
|
|
127
143
|
),
|
|
@@ -129,7 +145,6 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
|
|
|
129
145
|
// Styles.
|
|
130
146
|
EditorView.theme({
|
|
131
147
|
'.cm-scroller': {
|
|
132
|
-
paddingBottom: `${overscroll}px`,
|
|
133
148
|
scrollbarWidth: 'thin',
|
|
134
149
|
},
|
|
135
150
|
'.cm-scroller.cm-hide-scrollbar': {
|
|
@@ -138,7 +153,6 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
|
|
|
138
153
|
'.cm-scroller.cm-hide-scrollbar::-webkit-scrollbar': {
|
|
139
154
|
display: 'none',
|
|
140
155
|
},
|
|
141
|
-
|
|
142
156
|
'.cm-scroll-button': {
|
|
143
157
|
position: 'absolute',
|
|
144
158
|
bottom: '0.5rem',
|
|
@@ -147,11 +161,3 @@ export const autoScroll = ({ overscroll = 4 * lineHeight, throttle = 2_000 }: Pa
|
|
|
147
161
|
}),
|
|
148
162
|
];
|
|
149
163
|
};
|
|
150
|
-
|
|
151
|
-
const calcDistance = (scroller: HTMLElement) => {
|
|
152
|
-
const scrollTop = scroller.scrollTop;
|
|
153
|
-
const scrollHeight = scroller.scrollHeight;
|
|
154
|
-
const clientHeight = scroller.clientHeight;
|
|
155
|
-
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
|
|
156
|
-
return distanceFromBottom;
|
|
157
|
-
};
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { DeferredTask, Event, sleep } from '@dxos/async';
|
|
6
|
+
import { type Space } from '@dxos/client/echo';
|
|
7
|
+
import { type GossipMessage } from '@dxos/client/mesh';
|
|
6
8
|
import { Context } from '@dxos/context';
|
|
7
9
|
import { invariant } from '@dxos/invariant';
|
|
8
10
|
import { log } from '@dxos/log';
|
|
9
|
-
import { type Space } from '@dxos/react-client/echo';
|
|
10
|
-
import { type GossipMessage } from '@dxos/react-client/mesh';
|
|
11
11
|
|
|
12
12
|
import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from './awareness';
|
|
13
13
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
6
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
const paragraphBlockPlugin = ViewPlugin.fromClass(
|
|
11
|
+
class {
|
|
12
|
+
decorations: DecorationSet;
|
|
13
|
+
|
|
14
|
+
constructor(view: EditorView) {
|
|
15
|
+
this.decorations = this.build(view);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
update(update: ViewUpdate) {
|
|
19
|
+
if (update.docChanged || update.viewportChanged) {
|
|
20
|
+
this.decorations = this.build(update.view);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
build({ state }: EditorView) {
|
|
25
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
26
|
+
|
|
27
|
+
// Helper: commit a block from blockStart to endLine (inclusive).
|
|
28
|
+
const pushBlock = (fromLine: number, toLine: number) => {
|
|
29
|
+
// Add line decorations for each line in the block.
|
|
30
|
+
for (let lineNum = fromLine; lineNum <= toLine; lineNum++) {
|
|
31
|
+
const line = state.doc.line(lineNum);
|
|
32
|
+
builder.add(
|
|
33
|
+
line.from,
|
|
34
|
+
line.from,
|
|
35
|
+
Decoration.line({
|
|
36
|
+
class: mx(
|
|
37
|
+
'block-line',
|
|
38
|
+
fromLine === toLine && 'block-single',
|
|
39
|
+
lineNum === fromLine && 'block-first',
|
|
40
|
+
lineNum > fromLine && lineNum < toLine && 'block-middle',
|
|
41
|
+
lineNum === toLine && 'block-last',
|
|
42
|
+
),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let blockStart: number | null = null;
|
|
49
|
+
let consecutiveBlankLines = 0;
|
|
50
|
+
const totalLines = state.doc.lines;
|
|
51
|
+
for (let i = 1; i <= totalLines; i++) {
|
|
52
|
+
const line = state.doc.line(i);
|
|
53
|
+
const isBlank = /^\s*$/.test(line.text);
|
|
54
|
+
|
|
55
|
+
if (!isBlank) {
|
|
56
|
+
// Reset blank line counter.
|
|
57
|
+
consecutiveBlankLines = 0;
|
|
58
|
+
// Start a new block if we're not already in one.
|
|
59
|
+
if (blockStart === null) {
|
|
60
|
+
blockStart = i;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Increment blank line counter.
|
|
64
|
+
consecutiveBlankLines++;
|
|
65
|
+
|
|
66
|
+
// End the current block if we have 2+ consecutive blank lines.
|
|
67
|
+
if (consecutiveBlankLines >= 2 && blockStart !== null) {
|
|
68
|
+
pushBlock(blockStart, i - consecutiveBlankLines);
|
|
69
|
+
blockStart = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle any remaining block at the end of the document.
|
|
75
|
+
if (blockStart !== null) {
|
|
76
|
+
// Find the last non-blank line for the block end.
|
|
77
|
+
let lastNonBlankLine = totalLines;
|
|
78
|
+
while (lastNonBlankLine >= blockStart) {
|
|
79
|
+
const line = state.doc.line(lastNonBlankLine);
|
|
80
|
+
if (!/^\s*$/.test(line.text)) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
lastNonBlankLine--;
|
|
84
|
+
}
|
|
85
|
+
if (lastNonBlankLine >= blockStart) {
|
|
86
|
+
pushBlock(blockStart, lastNonBlankLine);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return builder.finish();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
decorations: (v) => v.decorations,
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
export const blocks = () => [
|
|
99
|
+
paragraphBlockPlugin,
|
|
100
|
+
EditorView.baseTheme({
|
|
101
|
+
'.cm-line.block-line': {
|
|
102
|
+
paddingLeft: '0.75rem',
|
|
103
|
+
paddingRight: '0.75rem',
|
|
104
|
+
borderLeft: '1px solid var(--dx-subduedSeparator)',
|
|
105
|
+
borderRight: '1px solid var(--dx-subduedSeparator)',
|
|
106
|
+
},
|
|
107
|
+
'.cm-line.block-single': {
|
|
108
|
+
border: '1px solid var(--dx-subduedSeparator)',
|
|
109
|
+
borderRadius: '6px',
|
|
110
|
+
paddingTop: '0.5rem',
|
|
111
|
+
paddingBottom: '0.5rem',
|
|
112
|
+
marginTop: '0.5rem',
|
|
113
|
+
marginBottom: '0.5rem',
|
|
114
|
+
},
|
|
115
|
+
'.cm-line.block-first': {
|
|
116
|
+
borderTop: '1px solid var(--dx-subduedSeparator)',
|
|
117
|
+
borderTopLeftRadius: '6px',
|
|
118
|
+
borderTopRightRadius: '6px',
|
|
119
|
+
paddingTop: '0.5rem',
|
|
120
|
+
marginTop: '0.5rem',
|
|
121
|
+
},
|
|
122
|
+
'.cm-line.block-middle': {},
|
|
123
|
+
'.cm-line.block-last': {
|
|
124
|
+
borderBottom: '1px solid var(--dx-subduedSeparator)',
|
|
125
|
+
borderBottomLeftRadius: '6px',
|
|
126
|
+
borderBottomRightRadius: '6px',
|
|
127
|
+
paddingBottom: '0.5rem',
|
|
128
|
+
marginBottom: '0.5rem',
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, Prec, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { keymap } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
type Bookmark = {
|
|
9
|
+
id: string;
|
|
10
|
+
pos: number;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const addBookmark = StateEffect.define<Bookmark>();
|
|
15
|
+
export const removeBookmark = StateEffect.define<string>();
|
|
16
|
+
export const clearBookmarks = StateEffect.define<void>();
|
|
17
|
+
|
|
18
|
+
export const bookmarks = (): Extension => {
|
|
19
|
+
return [
|
|
20
|
+
bookmarksField,
|
|
21
|
+
Prec.highest(
|
|
22
|
+
keymap.of([
|
|
23
|
+
{
|
|
24
|
+
key: 'Mod-ArrowUp',
|
|
25
|
+
run: (view) => {
|
|
26
|
+
const bookmarks = view.state.field(bookmarksField);
|
|
27
|
+
console.log('up', bookmarks);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'Mod-ArrowDown',
|
|
33
|
+
run: (view) => {
|
|
34
|
+
const bookmarks = view.state.field(bookmarksField);
|
|
35
|
+
console.log('down', bookmarks);
|
|
36
|
+
return true;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
]),
|
|
40
|
+
),
|
|
41
|
+
];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type BookmarkFieldState = {
|
|
45
|
+
bookmarks: Bookmark[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const bookmarksField = StateField.define<BookmarkFieldState>({
|
|
49
|
+
create: (): BookmarkFieldState => ({
|
|
50
|
+
bookmarks: [],
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
update: (value: BookmarkFieldState, tr): BookmarkFieldState => {
|
|
54
|
+
// Map bookmark positions through document changes.
|
|
55
|
+
let bookmarks = value.bookmarks.map((bookmark) => ({
|
|
56
|
+
...bookmark,
|
|
57
|
+
pos: tr.changes.mapPos(bookmark.pos),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// Process effects.
|
|
61
|
+
for (const effect of tr.effects) {
|
|
62
|
+
if (effect.is(addBookmark)) {
|
|
63
|
+
bookmarks = [...bookmarks, effect.value];
|
|
64
|
+
} else if (effect.is(removeBookmark)) {
|
|
65
|
+
bookmarks = bookmarks.filter((b) => b.id !== effect.value);
|
|
66
|
+
} else if (effect.is(clearBookmarks)) {
|
|
67
|
+
bookmarks = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sort bookmarks by position.
|
|
72
|
+
bookmarks.sort(({ pos: a }, { pos: b }) => a - b);
|
|
73
|
+
return { bookmarks };
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -104,13 +104,14 @@ export const commentsState = StateField.define<CommentsState>({
|
|
|
104
104
|
const styles = EditorView.theme({
|
|
105
105
|
'.cm-comment, .cm-comment-current': {
|
|
106
106
|
padding: '3px 0',
|
|
107
|
+
color: 'var(--dx-cmCommentText)',
|
|
107
108
|
backgroundColor: 'var(--dx-cmCommentSurface)',
|
|
108
109
|
},
|
|
109
110
|
'.cm-comment > span, .cm-comment-current > span': {
|
|
110
111
|
boxDecorationBreak: 'clone',
|
|
111
112
|
boxShadow: '0 0 1px 3px var(--dx-cmCommentSurface)',
|
|
112
113
|
backgroundColor: 'var(--dx-cmCommentSurface)',
|
|
113
|
-
color: 'var(--dx-
|
|
114
|
+
color: 'var(--dx-cmCommentText)',
|
|
114
115
|
cursor: 'pointer',
|
|
115
116
|
},
|
|
116
117
|
});
|
|
@@ -23,10 +23,10 @@ import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode'
|
|
|
23
23
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
24
24
|
import merge from 'lodash.merge';
|
|
25
25
|
|
|
26
|
+
import { type DocAccessor, type Space } from '@dxos/client/echo';
|
|
27
|
+
import { type Identity } from '@dxos/client/halo';
|
|
26
28
|
import { generateName } from '@dxos/display-name';
|
|
27
29
|
import { log } from '@dxos/log';
|
|
28
|
-
import { type DocAccessor, type Space } from '@dxos/react-client/echo';
|
|
29
|
-
import { type Identity } from '@dxos/react-client/halo';
|
|
30
30
|
import { type ThemeMode } from '@dxos/react-ui';
|
|
31
31
|
import { type HuePalette } from '@dxos/react-ui-theme';
|
|
32
32
|
import { hexToHue, isTruthy } from '@dxos/util';
|
|
@@ -87,7 +87,6 @@ export type BasicExtensionsOptions = {
|
|
|
87
87
|
lineNumbers?: boolean;
|
|
88
88
|
/** If false then do not set a max-width or side margin on the editor. */
|
|
89
89
|
lineWrapping?: boolean;
|
|
90
|
-
monospace?: boolean;
|
|
91
90
|
placeholder?: string;
|
|
92
91
|
/** If true user cannot edit the text, but they can still select and copy it. */
|
|
93
92
|
readOnly?: boolean;
|
|
@@ -136,7 +135,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
136
135
|
props.history && history(),
|
|
137
136
|
props.lineNumbers && [lineNumbers(), editorGutter],
|
|
138
137
|
props.lineWrapping && EditorView.lineWrapping,
|
|
139
|
-
props.monospace && editorMonospace,
|
|
140
138
|
props.placeholder && placeholder(props.placeholder),
|
|
141
139
|
props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
|
|
142
140
|
props.scrollPastEnd && scrollPastEnd(),
|
|
@@ -173,6 +171,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
173
171
|
|
|
174
172
|
export type ThemeExtensionsOptions = {
|
|
175
173
|
themeMode?: ThemeMode;
|
|
174
|
+
monospace?: boolean;
|
|
176
175
|
styles?: ThemeStyles;
|
|
177
176
|
syntaxHighlighting?: boolean;
|
|
178
177
|
slots?: {
|
|
@@ -180,6 +179,7 @@ export type ThemeExtensionsOptions = {
|
|
|
180
179
|
className?: string;
|
|
181
180
|
};
|
|
182
181
|
scroll?: {
|
|
182
|
+
// NOTE: Do not apply vertical padding to scroll container.
|
|
183
183
|
className?: string;
|
|
184
184
|
};
|
|
185
185
|
content?: {
|
|
@@ -212,6 +212,7 @@ export const defaultStyles = {
|
|
|
212
212
|
*/
|
|
213
213
|
export const createThemeExtensions = ({
|
|
214
214
|
themeMode,
|
|
215
|
+
monospace,
|
|
215
216
|
styles,
|
|
216
217
|
syntaxHighlighting: syntaxHighlightingProp,
|
|
217
218
|
slots: slotsParam,
|
|
@@ -220,6 +221,7 @@ export const createThemeExtensions = ({
|
|
|
220
221
|
return [
|
|
221
222
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
222
223
|
EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
|
|
224
|
+
monospace && editorMonospace,
|
|
223
225
|
syntaxHighlightingProp &&
|
|
224
226
|
syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
|
|
225
227
|
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
@@ -25,8 +25,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
|
|
|
25
25
|
foldGutter({
|
|
26
26
|
markerDOM: (open) => {
|
|
27
27
|
return renderRoot(
|
|
28
|
-
Domino.of('div').classNames('flex
|
|
29
|
-
// TODO(burdon): Use sprite directly.
|
|
28
|
+
Domino.of('div').classNames('flex bs-full items-center').build(),
|
|
30
29
|
<Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
|
|
31
30
|
);
|
|
32
31
|
},
|
package/src/extensions/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from './autoscroll';
|
|
|
8
8
|
export * from './automerge';
|
|
9
9
|
export * from './awareness';
|
|
10
10
|
export * from './blast';
|
|
11
|
+
export * from './blocks';
|
|
12
|
+
export * from './bookmarks';
|
|
11
13
|
export * from './comments';
|
|
12
14
|
export * from './debug';
|
|
13
15
|
export * from './dnd';
|
|
@@ -23,7 +25,9 @@ export * from './modes';
|
|
|
23
25
|
export * from './outliner';
|
|
24
26
|
export * from './popover';
|
|
25
27
|
export * from './preview';
|
|
28
|
+
export * from './replacer';
|
|
26
29
|
export * from './selection';
|
|
30
|
+
export * from './scrolling';
|
|
27
31
|
export * from './state';
|
|
28
32
|
export * from './tags';
|
|
29
33
|
export * from './typewriter';
|
|
@@ -5,34 +5,28 @@
|
|
|
5
5
|
import { type Extension } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
+
import { isNonNullable } from '@dxos/util';
|
|
9
|
+
|
|
8
10
|
import { documentId } from './selection';
|
|
9
11
|
|
|
10
12
|
export type ListenerOptions = {
|
|
11
|
-
onFocus?: (focusing: boolean) => void;
|
|
12
|
-
onChange?: (
|
|
13
|
+
onFocus?: (event: { id: string; focusing: boolean }) => void;
|
|
14
|
+
onChange?: (event: { id: string; text: string }) => void;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
/**
|
|
16
|
-
* Event listener.
|
|
17
|
-
* @deprecated Use EditorView.updateListener and listen for specific update events.
|
|
18
|
-
*/
|
|
19
17
|
export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
EditorView.focusChangeEffect.of((_, focusing) => {
|
|
25
|
-
onFocus(focusing);
|
|
18
|
+
return [
|
|
19
|
+
onFocus &&
|
|
20
|
+
EditorView.focusChangeEffect.of((state, focusing) => {
|
|
21
|
+
onFocus({ id: state.facet(documentId), focusing });
|
|
26
22
|
return null;
|
|
27
23
|
}),
|
|
28
|
-
);
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
onChange &&
|
|
26
|
+
EditorView.updateListener.of(({ state, docChanged }) => {
|
|
27
|
+
if (docChanged) {
|
|
28
|
+
onChange({ id: state.facet(documentId), text: state.doc.toString() });
|
|
29
|
+
}
|
|
34
30
|
}),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return extensions;
|
|
31
|
+
].filter(isNonNullable);
|
|
38
32
|
};
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { completionKeymap } from '@codemirror/autocomplete';
|
|
6
6
|
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
7
|
+
import { jsonLanguage } from '@codemirror/lang-json';
|
|
7
8
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|
8
|
-
import {
|
|
9
|
+
import { xml } from '@codemirror/lang-xml';
|
|
10
|
+
import { LanguageDescription, syntaxHighlighting } from '@codemirror/language';
|
|
9
11
|
import { languages } from '@codemirror/language-data';
|
|
10
12
|
import { type Extension } from '@codemirror/state';
|
|
11
13
|
import { keymap } from '@codemirror/view';
|
|
@@ -43,6 +45,7 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
|
|
|
43
45
|
base: markdownLanguage,
|
|
44
46
|
|
|
45
47
|
// Languages for syntax highlighting fenced code blocks.
|
|
48
|
+
defaultCodeLanguage: jsonLanguage,
|
|
46
49
|
codeLanguages: languages,
|
|
47
50
|
|
|
48
51
|
// Don't complete HTML tags.
|
|
@@ -74,6 +77,13 @@ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): E
|
|
|
74
77
|
];
|
|
75
78
|
};
|
|
76
79
|
|
|
80
|
+
const xmlLanguageDesc = LanguageDescription.of({
|
|
81
|
+
name: 'xml',
|
|
82
|
+
alias: ['html', 'xhtml'],
|
|
83
|
+
extensions: ['xml', 'xhtml'],
|
|
84
|
+
load: async () => xml(),
|
|
85
|
+
});
|
|
86
|
+
|
|
77
87
|
/**
|
|
78
88
|
* Default customizations.
|
|
79
89
|
* https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
|
|
@@ -91,5 +101,5 @@ const noSetExtHeading: MarkdownConfig = {
|
|
|
91
101
|
* Remove HTML and XML parsing.
|
|
92
102
|
*/
|
|
93
103
|
const noHtml: MarkdownConfig = {
|
|
94
|
-
remove: ['HTMLBlock', 'HTMLTag'],
|
|
104
|
+
// remove: ['HTMLBlock', 'HTMLTag'],
|
|
95
105
|
};
|