@tutorialkit-rb/react 1.5.2-rb.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/dist/BootScreen.d.ts +7 -0
- package/dist/BootScreen.js +37 -0
- package/dist/Button.d.ts +6 -0
- package/dist/Button.js +12 -0
- package/dist/Nav.d.ts +7 -0
- package/dist/Nav.js +38 -0
- package/dist/Panels/EditorPanel.d.ts +25 -0
- package/dist/Panels/EditorPanel.js +83 -0
- package/dist/Panels/PreviewPanel.d.ts +20 -0
- package/dist/Panels/PreviewPanel.js +166 -0
- package/dist/Panels/TerminalPanel.d.ts +7 -0
- package/dist/Panels/TerminalPanel.js +45 -0
- package/dist/Panels/WorkspacePanel.d.ts +14 -0
- package/dist/Panels/WorkspacePanel.js +166 -0
- package/dist/core/CodeMirrorEditor/BinaryContent.d.ts +1 -0
- package/dist/core/CodeMirrorEditor/BinaryContent.js +4 -0
- package/dist/core/CodeMirrorEditor/cm-theme.d.ts +8 -0
- package/dist/core/CodeMirrorEditor/cm-theme.js +167 -0
- package/dist/core/CodeMirrorEditor/indent.d.ts +2 -0
- package/dist/core/CodeMirrorEditor/indent.js +49 -0
- package/dist/core/CodeMirrorEditor/index.d.ts +39 -0
- package/dist/core/CodeMirrorEditor/index.js +201 -0
- package/dist/core/CodeMirrorEditor/languages.d.ts +3 -0
- package/dist/core/CodeMirrorEditor/languages.js +118 -0
- package/dist/core/CodeMirrorEditor/themes/vscode-dark.d.ts +2 -0
- package/dist/core/CodeMirrorEditor/themes/vscode-dark.js +75 -0
- package/dist/core/ContextMenu.d.ts +29 -0
- package/dist/core/ContextMenu.js +61 -0
- package/dist/core/Dialog.d.ts +15 -0
- package/dist/core/Dialog.js +12 -0
- package/dist/core/FileTree.d.ts +18 -0
- package/dist/core/FileTree.js +194 -0
- package/dist/core/Terminal/index.d.ts +15 -0
- package/dist/core/Terminal/index.js +57 -0
- package/dist/core/Terminal/theme.d.ts +2 -0
- package/dist/core/Terminal/theme.js +31 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +3 -0
- package/dist/hooks/useOutsideClick.d.ts +1 -0
- package/dist/hooks/useOutsideClick.js +14 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/styles/cm.css +100 -0
- package/dist/styles/nav.module.css +108 -0
- package/dist/styles/resize-panel.module.css +28 -0
- package/dist/styles/terminal.css +8 -0
- package/dist/utils/classnames.d.ts +16 -0
- package/dist/utils/classnames.js +47 -0
- package/dist/utils/debounce.d.ts +1 -0
- package/dist/utils/debounce.js +13 -0
- package/dist/utils/mobile.d.ts +1 -0
- package/dist/utils/mobile.js +4 -0
- package/package.json +116 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
5
|
+
import { DialogProvider } from '../core/Dialog.js';
|
|
6
|
+
import resizePanelStyles from '../styles/resize-panel.module.css';
|
|
7
|
+
import { classNames } from '../utils/classnames.js';
|
|
8
|
+
import { EditorPanel } from './EditorPanel.js';
|
|
9
|
+
import { PreviewPanel } from './PreviewPanel.js';
|
|
10
|
+
import { TerminalPanel } from './TerminalPanel.js';
|
|
11
|
+
const DEFAULT_TERMINAL_SIZE = 25;
|
|
12
|
+
/**
|
|
13
|
+
* This component is the orchestrator between various interactive components.
|
|
14
|
+
*/
|
|
15
|
+
export function WorkspacePanel({ tutorialStore, theme, dialog }) {
|
|
16
|
+
/**
|
|
17
|
+
* Re-render when lesson changes.
|
|
18
|
+
* The `tutorialStore.hasEditor()` and other methods below access
|
|
19
|
+
* stale data as they are not reactive.
|
|
20
|
+
*/
|
|
21
|
+
useStore(tutorialStore.ref);
|
|
22
|
+
const hasEditor = tutorialStore.hasEditor();
|
|
23
|
+
const hasPreviews = tutorialStore.hasPreviews();
|
|
24
|
+
const hideTerminalPanel = !tutorialStore.hasTerminalPanel();
|
|
25
|
+
const terminalPanelRef = useRef(null);
|
|
26
|
+
const terminalExpanded = useRef(false);
|
|
27
|
+
return (_jsxs(PanelGroup, { className: resizePanelStyles.PanelGroup, id: "right-panel-group", direction: "vertical", children: [_jsx(DialogProvider, { value: dialog, children: _jsx(EditorSection, { theme: theme, tutorialStore: tutorialStore, hasEditor: hasEditor, hasPreviews: hasPreviews, hideTerminalPanel: hideTerminalPanel }) }), _jsx(PanelResizeHandle, { className: resizePanelStyles.PanelResizeHandle, hitAreaMargins: { fine: 5, coarse: 5 }, disabled: !hasEditor }), _jsx(PreviewsSection, { theme: theme, tutorialStore: tutorialStore, terminalPanelRef: terminalPanelRef, terminalExpanded: terminalExpanded, hideTerminalPanel: hideTerminalPanel, hasPreviews: hasPreviews, hasEditor: hasEditor }), _jsx(PanelResizeHandle, { className: resizePanelStyles.PanelResizeHandle, hitAreaMargins: { fine: 5, coarse: 5 }, disabled: hideTerminalPanel || !hasPreviews }), _jsx(TerminalSection, { tutorialStore: tutorialStore, theme: theme, terminalPanelRef: terminalPanelRef, terminalExpanded: terminalExpanded, hideTerminalPanel: hideTerminalPanel, hasEditor: hasEditor, hasPreviews: hasPreviews })] }));
|
|
28
|
+
}
|
|
29
|
+
function EditorSection({ theme, tutorialStore, hasEditor }) {
|
|
30
|
+
const [helpAction, setHelpAction] = useState('reset');
|
|
31
|
+
const selectedFile = useStore(tutorialStore.selectedFile);
|
|
32
|
+
const currentDocument = useStore(tutorialStore.currentDocument);
|
|
33
|
+
const lessonFullyLoaded = useStore(tutorialStore.lessonFullyLoaded);
|
|
34
|
+
const editorConfig = useStore(tutorialStore.editorConfig);
|
|
35
|
+
const storeRef = useStore(tutorialStore.ref);
|
|
36
|
+
const files = useStore(tutorialStore.files);
|
|
37
|
+
const lesson = tutorialStore.lesson;
|
|
38
|
+
function onHelpClick() {
|
|
39
|
+
if (tutorialStore.hasSolution()) {
|
|
40
|
+
setHelpAction((action) => {
|
|
41
|
+
if (action === 'reset') {
|
|
42
|
+
tutorialStore.reset();
|
|
43
|
+
return 'solve';
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
tutorialStore.solve();
|
|
47
|
+
return 'reset';
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
tutorialStore.reset();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function onFileTreeChange({ method, type, value }) {
|
|
56
|
+
if (method === 'add' && type === 'file') {
|
|
57
|
+
return tutorialStore.addFile(value);
|
|
58
|
+
}
|
|
59
|
+
if (method === 'add' && type === 'folder') {
|
|
60
|
+
return tutorialStore.addFolder(value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (tutorialStore.hasSolution()) {
|
|
65
|
+
setHelpAction('solve');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
setHelpAction('reset');
|
|
69
|
+
}
|
|
70
|
+
}, [storeRef]);
|
|
71
|
+
return (_jsx(Panel, { id: hasEditor ? 'editor-opened' : 'editor-closed', defaultSize: hasEditor ? 50 : 0, minSize: 10, maxSize: hasEditor ? 100 : 0, collapsible: !hasEditor, className: "transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor", children: _jsx(EditorPanel, { id: storeRef, theme: theme, showFileTree: tutorialStore.hasFileTree(), editorDocument: currentDocument, files: files, i18n: lesson.data.i18n, hideRoot: lesson.data.hideRoot, helpAction: helpAction, onHelpClick: lessonFullyLoaded ? onHelpClick : undefined, onFileSelect: (filePath) => tutorialStore.setSelectedFile(filePath), onFileTreeChange: onFileTreeChange, allowEditPatterns: editorConfig.fileTree.allowEdits || undefined, selectedFile: selectedFile, onEditorScroll: (position) => tutorialStore.setCurrentDocumentScrollPosition(position), onEditorChange: (update) => tutorialStore.setCurrentDocumentContent(update.content) }) }));
|
|
72
|
+
}
|
|
73
|
+
function PreviewsSection({ tutorialStore, terminalPanelRef, terminalExpanded, hideTerminalPanel, hasPreviews, hasEditor, }) {
|
|
74
|
+
const previewRef = useRef(null);
|
|
75
|
+
const lesson = tutorialStore.lesson;
|
|
76
|
+
const terminalConfig = useStore(tutorialStore.terminalConfig);
|
|
77
|
+
const storeRef = useStore(tutorialStore.ref);
|
|
78
|
+
function showTerminal() {
|
|
79
|
+
const { current: terminal } = terminalPanelRef;
|
|
80
|
+
if (!terminal) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!terminalExpanded.current) {
|
|
84
|
+
terminalExpanded.current = true;
|
|
85
|
+
terminal.resize(DEFAULT_TERMINAL_SIZE);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
terminal.expand();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const toggleTerminal = useCallback(() => {
|
|
92
|
+
if (terminalPanelRef.current?.isCollapsed()) {
|
|
93
|
+
showTerminal();
|
|
94
|
+
}
|
|
95
|
+
else if (terminalPanelRef.current) {
|
|
96
|
+
terminalPanelRef.current.collapse();
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (hideTerminalPanel) {
|
|
101
|
+
// force hide the terminal if we don't have any panels to show
|
|
102
|
+
terminalPanelRef.current?.collapse();
|
|
103
|
+
terminalExpanded.current = false;
|
|
104
|
+
}
|
|
105
|
+
}, [hideTerminalPanel]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (terminalConfig.defaultOpen) {
|
|
108
|
+
showTerminal();
|
|
109
|
+
}
|
|
110
|
+
}, [terminalConfig.defaultOpen]);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const lesson = tutorialStore.lesson;
|
|
113
|
+
const unsubscribe = tutorialStore.lessonFullyLoaded.subscribe((loaded) => {
|
|
114
|
+
if (loaded && lesson.data.autoReload) {
|
|
115
|
+
previewRef.current?.reload();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return () => unsubscribe();
|
|
119
|
+
}, [storeRef]);
|
|
120
|
+
const MIN_SIZE_IN_PIXELS = 38;
|
|
121
|
+
const [panelMinSize, setPanelMinSize] = useState(10);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const panelGroup = document.querySelector('div[data-panel-group-id="right-panel-group"]');
|
|
124
|
+
if (!panelGroup) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const observer = new ResizeObserver(() => {
|
|
128
|
+
const height = panelGroup?.offsetHeight;
|
|
129
|
+
setPanelMinSize((MIN_SIZE_IN_PIXELS / height) * 100);
|
|
130
|
+
});
|
|
131
|
+
observer.observe(panelGroup);
|
|
132
|
+
return () => {
|
|
133
|
+
observer.disconnect();
|
|
134
|
+
};
|
|
135
|
+
}, []);
|
|
136
|
+
return (_jsx(Panel, { id: hasPreviews ? 'previews-opened' : 'previews-closed', defaultSize: hasPreviews ? 50 : 0, minSize: panelMinSize, maxSize: hasPreviews ? 100 : 0, collapsible: !hasPreviews, className: classNames({
|
|
137
|
+
'transition-theme border-t border-tk-elements-app-borderColor': hasEditor,
|
|
138
|
+
}), children: _jsx(PreviewPanel, { ref: previewRef, tutorialStore: tutorialStore, i18n: lesson.data.i18n, showToggleTerminal: !hideTerminalPanel, toggleTerminal: toggleTerminal }) }));
|
|
139
|
+
}
|
|
140
|
+
function TerminalSection({ tutorialStore, theme, terminalPanelRef, terminalExpanded, hideTerminalPanel, hasEditor, hasPreviews, }) {
|
|
141
|
+
let id = 'terminal-closed';
|
|
142
|
+
if (hideTerminalPanel) {
|
|
143
|
+
id = 'terminal-none';
|
|
144
|
+
}
|
|
145
|
+
else if (!hasPreviews && !hasEditor) {
|
|
146
|
+
id = 'terminal-full';
|
|
147
|
+
}
|
|
148
|
+
else if (!hasPreviews) {
|
|
149
|
+
id = 'terminal-opened';
|
|
150
|
+
}
|
|
151
|
+
let defaultSize = 0;
|
|
152
|
+
if (hideTerminalPanel) {
|
|
153
|
+
defaultSize = 0;
|
|
154
|
+
}
|
|
155
|
+
else if (!hasPreviews && !hasEditor) {
|
|
156
|
+
defaultSize = 100;
|
|
157
|
+
}
|
|
158
|
+
else if (!hasPreviews) {
|
|
159
|
+
defaultSize = DEFAULT_TERMINAL_SIZE;
|
|
160
|
+
}
|
|
161
|
+
return (_jsx(Panel, { id: id, defaultSize: defaultSize, minSize: hideTerminalPanel ? 0 : 10, collapsible: hasPreviews, ref: terminalPanelRef, onExpand: () => {
|
|
162
|
+
terminalExpanded.current = true;
|
|
163
|
+
}, className: classNames('transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor', {
|
|
164
|
+
'border-t border-tk-elements-app-borderColor': hasPreviews,
|
|
165
|
+
}), children: _jsx(TerminalPanel, { tutorialStore: tutorialStore, theme: theme }) }));
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function BinaryContent(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function BinaryContent() {
|
|
3
|
+
return (_jsx("div", { className: "flex items-center justify-center absolute inset-0 z-10 text-sm bg-tk-elements-app-backgroundColor text-tk-elements-app-textColor", children: "File format cannot be displayed." }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Compartment, type Extension } from '@codemirror/state';
|
|
2
|
+
import '../../styles/cm.css';
|
|
3
|
+
import type { Theme } from '../types.js';
|
|
4
|
+
import type { EditorSettings } from './index.js';
|
|
5
|
+
export declare const darkTheme: Extension;
|
|
6
|
+
export declare const themeSelection: Compartment;
|
|
7
|
+
export declare function getTheme(theme: Theme, settings?: EditorSettings): Extension;
|
|
8
|
+
export declare function reconfigureTheme(theme: Theme): import("@codemirror/state").StateEffect<unknown>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
2
|
+
import { Compartment } from '@codemirror/state';
|
|
3
|
+
import { EditorView } from '@codemirror/view';
|
|
4
|
+
import { transitionTheme } from '@tutorialkit-rb/theme/transition-theme';
|
|
5
|
+
import '../../styles/cm.css';
|
|
6
|
+
import { vscodeDarkTheme } from './themes/vscode-dark.js';
|
|
7
|
+
export const darkTheme = EditorView.theme({}, { dark: true });
|
|
8
|
+
export const themeSelection = new Compartment();
|
|
9
|
+
export function getTheme(theme, settings = {}) {
|
|
10
|
+
return [
|
|
11
|
+
getEditorTheme(settings),
|
|
12
|
+
theme === 'dark' ? themeSelection.of([getDarkTheme()]) : themeSelection.of([getLightTheme()]),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
export function reconfigureTheme(theme) {
|
|
16
|
+
return themeSelection.reconfigure(theme === 'dark' ? getDarkTheme() : getLightTheme());
|
|
17
|
+
}
|
|
18
|
+
function getEditorTheme(settings) {
|
|
19
|
+
return EditorView.theme({
|
|
20
|
+
...(settings.fontSize && {
|
|
21
|
+
'&': {
|
|
22
|
+
fontSize: settings.fontSize,
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
'&.cm-editor': {
|
|
26
|
+
height: '100%',
|
|
27
|
+
background: 'var(--cm-backgroundColor)',
|
|
28
|
+
color: 'var(--cm-textColor)',
|
|
29
|
+
...transitionTheme,
|
|
30
|
+
},
|
|
31
|
+
'.cm-cursor': {
|
|
32
|
+
borderLeft: 'var(--cm-cursor-width) solid var(--cm-cursor-backgroundColor)',
|
|
33
|
+
},
|
|
34
|
+
'.cm-scroller': {
|
|
35
|
+
lineHeight: '1.5',
|
|
36
|
+
},
|
|
37
|
+
'.cm-line': {
|
|
38
|
+
padding: '0 0 0 4px',
|
|
39
|
+
},
|
|
40
|
+
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
|
41
|
+
backgroundColor: 'var(--cm-selection-backgroundColorFocused)',
|
|
42
|
+
opacity: 'var(--cm-selection-backgroundOpacityFocused, 0.3)',
|
|
43
|
+
...transitionTheme,
|
|
44
|
+
},
|
|
45
|
+
'&:not(.cm-focused) > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
|
46
|
+
backgroundColor: 'var(--cm-selection-backgroundColorBlured)',
|
|
47
|
+
opacity: 'var(--cm-selection-backgroundOpacityBlured, 0.3)',
|
|
48
|
+
...transitionTheme,
|
|
49
|
+
},
|
|
50
|
+
'&.cm-focused > .cm-scroller .cm-matchingBracket': {
|
|
51
|
+
backgroundColor: 'var(--cm-matching-bracket)',
|
|
52
|
+
},
|
|
53
|
+
'.cm-activeLine': {
|
|
54
|
+
background: 'var(--cm-activeLineBackgroundColor)',
|
|
55
|
+
...transitionTheme,
|
|
56
|
+
},
|
|
57
|
+
'.cm-gutters': {
|
|
58
|
+
background: 'var(--cm-gutter-backgroundColor)',
|
|
59
|
+
borderRight: 0,
|
|
60
|
+
color: 'var(--cm-gutter-textColor)',
|
|
61
|
+
...transitionTheme,
|
|
62
|
+
},
|
|
63
|
+
'.cm-gutter': {
|
|
64
|
+
'&.cm-lineNumbers': {
|
|
65
|
+
fontFamily: 'Roboto Mono, monospace',
|
|
66
|
+
fontSize: '13px',
|
|
67
|
+
minWidth: '28px',
|
|
68
|
+
},
|
|
69
|
+
'& .cm-activeLineGutter': {
|
|
70
|
+
background: 'transparent',
|
|
71
|
+
color: 'var(--cm-gutter-activeLineTextColor)',
|
|
72
|
+
},
|
|
73
|
+
'&.cm-foldGutter .cm-gutterElement > .fold-icon': {
|
|
74
|
+
cursor: 'pointer',
|
|
75
|
+
color: 'var(--cm-foldGutter-textColor)',
|
|
76
|
+
transform: 'translateY(2px)',
|
|
77
|
+
'&:hover': {
|
|
78
|
+
color: 'var(--cm-foldGutter-textColorHover)',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
'.cm-foldGutter .cm-gutterElement': {
|
|
83
|
+
padding: '0 4px',
|
|
84
|
+
},
|
|
85
|
+
'.cm-tooltip-autocomplete > ul > li': {
|
|
86
|
+
minHeight: '18px',
|
|
87
|
+
},
|
|
88
|
+
'.cm-panel.cm-search label': {
|
|
89
|
+
marginLeft: '2px',
|
|
90
|
+
},
|
|
91
|
+
'.cm-panel.cm-search input[type=checkbox]': {
|
|
92
|
+
position: 'relative',
|
|
93
|
+
transform: 'translateY(2px)',
|
|
94
|
+
marginRight: '4px',
|
|
95
|
+
},
|
|
96
|
+
'.cm-panels': {
|
|
97
|
+
borderColor: 'var(--cm-panels-borderColor)',
|
|
98
|
+
},
|
|
99
|
+
'.cm-panel.cm-search': {
|
|
100
|
+
background: 'var(--cm-search-backgroundColor)',
|
|
101
|
+
color: 'var(--cm-search-textColor)',
|
|
102
|
+
padding: '6px 8px',
|
|
103
|
+
...transitionTheme,
|
|
104
|
+
},
|
|
105
|
+
'.cm-search .cm-button': {
|
|
106
|
+
background: 'var(--cm-search-button-backgroundColor)',
|
|
107
|
+
borderColor: 'var(--cm-search-button-borderColor)',
|
|
108
|
+
color: 'var(--cm-search-button-textColor)',
|
|
109
|
+
borderRadius: '4px',
|
|
110
|
+
'&:hover': {
|
|
111
|
+
color: 'var(--cm-search-button-textColorHover)',
|
|
112
|
+
},
|
|
113
|
+
'&:focus-visible': {
|
|
114
|
+
outline: 'none',
|
|
115
|
+
borderColor: 'var(--cm-search-button-borderColorFocused)',
|
|
116
|
+
},
|
|
117
|
+
'&:hover:not(:focus-visible)': {
|
|
118
|
+
background: 'var(--cm-search-button-backgroundColorHover)',
|
|
119
|
+
borderColor: 'var(--cm-search-button-borderColorHover)',
|
|
120
|
+
},
|
|
121
|
+
'&:hover:focus-visible': {
|
|
122
|
+
background: 'var(--cm-search-button-backgroundColorHover)',
|
|
123
|
+
borderColor: 'var(--cm-search-button-borderColorFocused)',
|
|
124
|
+
},
|
|
125
|
+
...transitionTheme,
|
|
126
|
+
},
|
|
127
|
+
'.cm-panel.cm-search [name=close]': {
|
|
128
|
+
top: '6px',
|
|
129
|
+
right: '6px',
|
|
130
|
+
padding: '0 6px',
|
|
131
|
+
backgroundColor: 'var(--cm-search-closeButton-backgroundColor)',
|
|
132
|
+
color: 'var(--cm-search-closeButton-textColor)',
|
|
133
|
+
'&:hover': {
|
|
134
|
+
'border-radius': '6px',
|
|
135
|
+
color: 'var(--cm-search-closeButton-textColorHover)',
|
|
136
|
+
backgroundColor: 'var(--cm-search-closeButton-backgroundColorHover)',
|
|
137
|
+
...transitionTheme,
|
|
138
|
+
},
|
|
139
|
+
...transitionTheme,
|
|
140
|
+
},
|
|
141
|
+
'.cm-search input': {
|
|
142
|
+
background: 'var(--cm-search-input-backgroundColor)',
|
|
143
|
+
borderColor: 'var(--cm-search-input-borderColor)',
|
|
144
|
+
outline: 'none',
|
|
145
|
+
borderRadius: '4px',
|
|
146
|
+
'&:focus-visible': {
|
|
147
|
+
borderColor: 'var(--cm-search-input-borderColorFocused)',
|
|
148
|
+
...transitionTheme,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
'.cm-tooltip': {
|
|
152
|
+
background: 'var(--cm-tooltip-backgroundColor)',
|
|
153
|
+
borderColor: 'var(--cm-tooltip-borderColor)',
|
|
154
|
+
color: 'var(--cm-tooltip-textColor)',
|
|
155
|
+
},
|
|
156
|
+
'.cm-tooltip.cm-tooltip-autocomplete ul li[aria-selected]': {
|
|
157
|
+
background: 'var(--cm-tooltip-backgroundColorSelected)',
|
|
158
|
+
color: 'var(--cm-tooltip-textColorSelected)',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function getLightTheme() {
|
|
163
|
+
return syntaxHighlighting(defaultHighlightStyle);
|
|
164
|
+
}
|
|
165
|
+
function getDarkTheme() {
|
|
166
|
+
return syntaxHighlighting(vscodeDarkTheme);
|
|
167
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { indentLess } from '@codemirror/commands';
|
|
2
|
+
import { indentUnit } from '@codemirror/language';
|
|
3
|
+
import { EditorSelection, EditorState, Line } from '@codemirror/state';
|
|
4
|
+
import { EditorView } from '@codemirror/view';
|
|
5
|
+
export const indentKeyBinding = {
|
|
6
|
+
key: 'Tab',
|
|
7
|
+
run: indentMore,
|
|
8
|
+
shift: indentLess,
|
|
9
|
+
};
|
|
10
|
+
function indentMore({ state, dispatch }) {
|
|
11
|
+
if (state.readOnly) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
dispatch(state.update(changeBySelectedLine(state, (from, to, changes) => {
|
|
15
|
+
changes.push({ from, to, insert: state.facet(indentUnit) });
|
|
16
|
+
}), { userEvent: 'input.indent' }));
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
function changeBySelectedLine(state, cb) {
|
|
20
|
+
return state.changeByRange((range) => {
|
|
21
|
+
const changes = [];
|
|
22
|
+
const line = state.doc.lineAt(range.from);
|
|
23
|
+
// just insert single indent unit at the current cursor position
|
|
24
|
+
if (range.from === range.to) {
|
|
25
|
+
cb(range.from, undefined, changes, line);
|
|
26
|
+
}
|
|
27
|
+
// handle the case when multiple characters are selected in a single line
|
|
28
|
+
else if (range.from < range.to && range.to <= line.to) {
|
|
29
|
+
cb(range.from, range.to, changes, line);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
let atLine = -1;
|
|
33
|
+
// handle the case when selection spans multiple lines
|
|
34
|
+
for (let pos = range.from; pos <= range.to;) {
|
|
35
|
+
const line = state.doc.lineAt(pos);
|
|
36
|
+
if (line.number > atLine && (range.empty || range.to > line.from)) {
|
|
37
|
+
cb(line.from, undefined, changes, line);
|
|
38
|
+
atLine = line.number;
|
|
39
|
+
}
|
|
40
|
+
pos = line.to + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const changeSet = state.changes(changes);
|
|
44
|
+
return {
|
|
45
|
+
changes,
|
|
46
|
+
range: EditorSelection.range(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EditorSelection } from '@codemirror/state';
|
|
2
|
+
import type { Theme } from '../types.js';
|
|
3
|
+
export interface EditorDocument {
|
|
4
|
+
value: string | Uint8Array;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
filePath: string;
|
|
7
|
+
scroll?: ScrollPosition;
|
|
8
|
+
}
|
|
9
|
+
export interface EditorSettings {
|
|
10
|
+
fontSize?: string;
|
|
11
|
+
tabSize?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ScrollPosition {
|
|
14
|
+
top: number;
|
|
15
|
+
left: number;
|
|
16
|
+
}
|
|
17
|
+
export interface EditorUpdate {
|
|
18
|
+
selection: EditorSelection;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
export type OnChangeCallback = (update: EditorUpdate) => void;
|
|
22
|
+
export type OnScrollCallback = (position: ScrollPosition) => void;
|
|
23
|
+
interface Props {
|
|
24
|
+
theme: Theme;
|
|
25
|
+
id?: unknown;
|
|
26
|
+
doc?: EditorDocument;
|
|
27
|
+
debounceChange?: number;
|
|
28
|
+
debounceScroll?: number;
|
|
29
|
+
autoFocusOnDocumentChange?: boolean;
|
|
30
|
+
onChange?: OnChangeCallback;
|
|
31
|
+
onScroll?: OnScrollCallback;
|
|
32
|
+
className?: string;
|
|
33
|
+
settings?: EditorSettings;
|
|
34
|
+
}
|
|
35
|
+
export declare function CodeMirrorEditor({ id, doc, debounceScroll, debounceChange, autoFocusOnDocumentChange, onScroll, onChange, theme, settings, className, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
36
|
+
export declare namespace CodeMirrorEditor {
|
|
37
|
+
var displayName: string;
|
|
38
|
+
}
|
|
39
|
+
export default CodeMirrorEditor;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { acceptCompletion, autocompletion, closeBrackets } from '@codemirror/autocomplete';
|
|
3
|
+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
|
4
|
+
import { bracketMatching, foldGutter, indentOnInput, indentUnit } from '@codemirror/language';
|
|
5
|
+
import { searchKeymap } from '@codemirror/search';
|
|
6
|
+
import { Compartment, EditorSelection, EditorState } from '@codemirror/state';
|
|
7
|
+
import { EditorView, drawSelection, dropCursor, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, scrollPastEnd, } from '@codemirror/view';
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { classNames } from '../../utils/classnames.js';
|
|
10
|
+
import { debounce } from '../../utils/debounce.js';
|
|
11
|
+
import { BinaryContent } from './BinaryContent.js';
|
|
12
|
+
import { getTheme, reconfigureTheme } from './cm-theme.js';
|
|
13
|
+
import { indentKeyBinding } from './indent.js';
|
|
14
|
+
import { getLanguage } from './languages.js';
|
|
15
|
+
export function CodeMirrorEditor({ id, doc, debounceScroll = 100, debounceChange = 150, autoFocusOnDocumentChange = false, onScroll, onChange, theme, settings, className = '', }) {
|
|
16
|
+
const [language] = useState(new Compartment());
|
|
17
|
+
const [readOnly] = useState(new Compartment());
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
const viewRef = useRef();
|
|
20
|
+
const themeRef = useRef();
|
|
21
|
+
const docRef = useRef();
|
|
22
|
+
const editorStatesRef = useRef();
|
|
23
|
+
const onScrollRef = useRef(onScroll);
|
|
24
|
+
const onChangeRef = useRef(onChange);
|
|
25
|
+
const isBinaryFile = doc?.value instanceof Uint8Array;
|
|
26
|
+
onScrollRef.current = onScroll;
|
|
27
|
+
onChangeRef.current = onChange;
|
|
28
|
+
docRef.current = doc;
|
|
29
|
+
themeRef.current = theme;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const onUpdate = debounce((update) => {
|
|
32
|
+
onChangeRef.current?.(update);
|
|
33
|
+
}, debounceChange);
|
|
34
|
+
const view = new EditorView({
|
|
35
|
+
parent: containerRef.current,
|
|
36
|
+
dispatchTransactions(transactions) {
|
|
37
|
+
const previousSelection = view.state.selection;
|
|
38
|
+
view.update(transactions);
|
|
39
|
+
const newSelection = view.state.selection;
|
|
40
|
+
const selectionChanged = newSelection !== previousSelection &&
|
|
41
|
+
(newSelection === undefined || previousSelection === undefined || !newSelection.eq(previousSelection));
|
|
42
|
+
if (docRef.current &&
|
|
43
|
+
!docRef.current.loading &&
|
|
44
|
+
(transactions.some((transaction) => transaction.docChanged) || selectionChanged)) {
|
|
45
|
+
onUpdate({
|
|
46
|
+
selection: view.state.selection,
|
|
47
|
+
content: view.state.doc.toString(),
|
|
48
|
+
});
|
|
49
|
+
editorStatesRef.current.set(docRef.current.filePath, view.state);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
viewRef.current = view;
|
|
54
|
+
// we grab the style tag that codemirror mounts
|
|
55
|
+
const codemirrorStyleTag = document.head.children[0];
|
|
56
|
+
codemirrorStyleTag.setAttribute('data-astro-transition-persist', 'codemirror');
|
|
57
|
+
return () => {
|
|
58
|
+
viewRef.current?.destroy();
|
|
59
|
+
viewRef.current = undefined;
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!viewRef.current) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
viewRef.current.dispatch({
|
|
67
|
+
effects: [reconfigureTheme(theme)],
|
|
68
|
+
});
|
|
69
|
+
}, [theme]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
editorStatesRef.current = new Map();
|
|
72
|
+
}, [id]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const editorStates = editorStatesRef.current;
|
|
75
|
+
const view = viewRef.current;
|
|
76
|
+
const theme = themeRef.current;
|
|
77
|
+
if (!doc) {
|
|
78
|
+
setNoDocument(view);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (doc.value instanceof Uint8Array) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let state = editorStates.get(doc.filePath);
|
|
85
|
+
if (!state) {
|
|
86
|
+
state = newEditorState(doc.value, theme, settings, onScrollRef, debounceScroll, [
|
|
87
|
+
language.of([]),
|
|
88
|
+
readOnly.of([EditorState.readOnly.of(doc.loading)]),
|
|
89
|
+
]);
|
|
90
|
+
editorStates.set(doc.filePath, state);
|
|
91
|
+
}
|
|
92
|
+
view.setState(state);
|
|
93
|
+
setEditorDocument(view, theme, language, readOnly, autoFocusOnDocumentChange, doc);
|
|
94
|
+
}, [doc?.value, doc?.filePath, doc?.loading, autoFocusOnDocumentChange]);
|
|
95
|
+
return (_jsxs("div", { className: classNames('relative', className), children: [isBinaryFile && _jsx(BinaryContent, {}), _jsx("div", { className: "h-full overflow-hidden", ref: containerRef })] }));
|
|
96
|
+
}
|
|
97
|
+
export default CodeMirrorEditor;
|
|
98
|
+
CodeMirrorEditor.displayName = 'CodeMirrorEditor';
|
|
99
|
+
function newEditorState(content, theme, settings, onScrollRef, debounceScroll, extensions) {
|
|
100
|
+
return EditorState.create({
|
|
101
|
+
doc: content,
|
|
102
|
+
extensions: [
|
|
103
|
+
EditorView.contentAttributes.of({ 'aria-label': 'Editor' }),
|
|
104
|
+
EditorView.domEventHandlers({
|
|
105
|
+
scroll: debounce((_event, view) => {
|
|
106
|
+
onScrollRef.current?.({ left: view.scrollDOM.scrollLeft, top: view.scrollDOM.scrollTop });
|
|
107
|
+
}, debounceScroll),
|
|
108
|
+
keydown: (event) => {
|
|
109
|
+
if (event.code === 'KeyS' && (event.ctrlKey || event.metaKey)) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
getTheme(theme, settings),
|
|
115
|
+
history(),
|
|
116
|
+
keymap.of([
|
|
117
|
+
...defaultKeymap,
|
|
118
|
+
...historyKeymap,
|
|
119
|
+
...searchKeymap,
|
|
120
|
+
{ key: 'Tab', run: acceptCompletion },
|
|
121
|
+
indentKeyBinding,
|
|
122
|
+
]),
|
|
123
|
+
indentUnit.of('\t'),
|
|
124
|
+
autocompletion({
|
|
125
|
+
closeOnBlur: false,
|
|
126
|
+
}),
|
|
127
|
+
closeBrackets(),
|
|
128
|
+
lineNumbers(),
|
|
129
|
+
scrollPastEnd(),
|
|
130
|
+
dropCursor(),
|
|
131
|
+
drawSelection(),
|
|
132
|
+
bracketMatching(),
|
|
133
|
+
EditorState.tabSize.of(settings?.tabSize ?? 2),
|
|
134
|
+
indentOnInput(),
|
|
135
|
+
highlightActiveLineGutter(),
|
|
136
|
+
highlightActiveLine(),
|
|
137
|
+
foldGutter({
|
|
138
|
+
markerDOM: (open) => {
|
|
139
|
+
const icon = document.createElement('div');
|
|
140
|
+
icon.className = `fold-icon ${open ? 'i-ph-caret-down-bold' : 'i-ph-caret-right-bold'}`;
|
|
141
|
+
return icon;
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
...extensions,
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function setNoDocument(view) {
|
|
149
|
+
view.dispatch({
|
|
150
|
+
selection: { anchor: 0 },
|
|
151
|
+
changes: {
|
|
152
|
+
from: 0,
|
|
153
|
+
to: view.state.doc.length,
|
|
154
|
+
insert: '',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
view.scrollDOM.scrollTo(0, 0);
|
|
158
|
+
}
|
|
159
|
+
function setEditorDocument(view, theme, language, readOnly, autoFocus, doc) {
|
|
160
|
+
if (doc.value !== view.state.doc.toString()) {
|
|
161
|
+
view.dispatch({
|
|
162
|
+
selection: { anchor: 0 },
|
|
163
|
+
changes: {
|
|
164
|
+
from: 0,
|
|
165
|
+
to: view.state.doc.length,
|
|
166
|
+
insert: doc.value,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
view.dispatch({
|
|
171
|
+
effects: [readOnly.reconfigure([EditorState.readOnly.of(doc.loading)])],
|
|
172
|
+
});
|
|
173
|
+
getLanguage(doc.filePath).then((languageSupport) => {
|
|
174
|
+
if (!languageSupport) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
view.dispatch({
|
|
178
|
+
effects: [language.reconfigure([languageSupport]), reconfigureTheme(theme)],
|
|
179
|
+
});
|
|
180
|
+
requestAnimationFrame(() => {
|
|
181
|
+
const currentLeft = view.scrollDOM.scrollLeft;
|
|
182
|
+
const currentTop = view.scrollDOM.scrollTop;
|
|
183
|
+
const newLeft = doc.scroll?.left ?? 0;
|
|
184
|
+
const newTop = doc.scroll?.top ?? 0;
|
|
185
|
+
const needsScrolling = currentLeft !== newLeft || currentTop !== newTop;
|
|
186
|
+
if (autoFocus) {
|
|
187
|
+
if (needsScrolling) {
|
|
188
|
+
// we have to wait until the scroll position was changed before we can set the focus
|
|
189
|
+
view.scrollDOM.addEventListener('scroll', () => {
|
|
190
|
+
view.focus();
|
|
191
|
+
}, { once: true });
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// if the scroll position is still the same we can focus immediately
|
|
195
|
+
view.focus();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
view.scrollDOM.scrollTo(newLeft, newTop);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|