@podlite/editor-react 0.0.40 → 0.0.42
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/CHANGELOG.podlite +18 -0
- package/esm/Editor.d.ts +5 -0
- package/esm/Editor.js +175 -39
- package/esm/Editor.js.map +1 -1
- package/esm/Wrapper.d.ts +5 -3
- package/esm/Wrapper.js +55 -76
- package/esm/Wrapper.js.map +1 -1
- package/esm/foldPodlite.d.ts +2 -0
- package/esm/foldPodlite.js +108 -0
- package/esm/foldPodlite.js.map +1 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js.map +1 -1
- package/esm/listContinuation.d.ts +3 -0
- package/esm/listContinuation.js +124 -0
- package/esm/listContinuation.js.map +1 -0
- package/esm/types.d.ts +12 -0
- package/esm/types.js +2 -0
- package/esm/types.js.map +1 -0
- package/lib/Editor.d.ts +5 -0
- package/lib/Editor.js +175 -39
- package/lib/Editor.js.map +1 -1
- package/lib/Wrapper.d.ts +5 -3
- package/lib/Wrapper.js +54 -75
- package/lib/Wrapper.js.map +1 -1
- package/lib/foldPodlite.d.ts +2 -0
- package/lib/foldPodlite.js +111 -0
- package/lib/foldPodlite.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js.map +1 -1
- package/lib/listContinuation.d.ts +3 -0
- package/lib/listContinuation.js +127 -0
- package/lib/listContinuation.js.map +1 -0
- package/lib/types.d.ts +12 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/package.json +25 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { foldService } from '@codemirror/language';
|
|
2
|
+
// =begin Name ... =end Name
|
|
3
|
+
const beginRe = /^(\s*)=begin\s+(\S+)/;
|
|
4
|
+
const endRe = /^(\s*)=end\s+(\S+)/;
|
|
5
|
+
// =headN (level 1-6)
|
|
6
|
+
const headRe = /^(\s*)=head(\d+)\s/;
|
|
7
|
+
// Verbatim block types where content should not be folded
|
|
8
|
+
const verbatimTypes = new Set(['code', 'comment', 'data', 'input', 'output']);
|
|
9
|
+
// Parse =begin line, return block name or null
|
|
10
|
+
function parseBeginLine(lineText) {
|
|
11
|
+
const m = lineText.match(beginRe);
|
|
12
|
+
return m ? m[2] ?? null : null;
|
|
13
|
+
}
|
|
14
|
+
// Cache verbatim ranges per document version to avoid rescanning on every fold query
|
|
15
|
+
let _cachedVerbatimDoc = null;
|
|
16
|
+
let _cachedVerbatimRanges = [];
|
|
17
|
+
function getVerbatimRanges(state) {
|
|
18
|
+
// EditorState.doc is immutable — same reference means same content
|
|
19
|
+
if (_cachedVerbatimDoc === state.doc)
|
|
20
|
+
return _cachedVerbatimRanges;
|
|
21
|
+
const ranges = [];
|
|
22
|
+
for (let i = 1; i <= state.doc.lines; i++) {
|
|
23
|
+
const line = state.doc.line(i);
|
|
24
|
+
const m = line.text.match(beginRe);
|
|
25
|
+
if (m && verbatimTypes.has(m[2])) {
|
|
26
|
+
for (let j = i + 1; j <= state.doc.lines; j++) {
|
|
27
|
+
const endLine = state.doc.line(j);
|
|
28
|
+
const em = endLine.text.match(endRe);
|
|
29
|
+
if (em && em[2] === m[2]) {
|
|
30
|
+
if (i + 1 <= j - 1) {
|
|
31
|
+
ranges.push({ from: i + 1, to: j - 1 });
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
_cachedVerbatimDoc = state.doc;
|
|
39
|
+
_cachedVerbatimRanges = ranges;
|
|
40
|
+
return ranges;
|
|
41
|
+
}
|
|
42
|
+
function isInsideVerbatim(lineNumber, ranges) {
|
|
43
|
+
return ranges.some(r => lineNumber >= r.from && lineNumber <= r.to);
|
|
44
|
+
}
|
|
45
|
+
// Fold =begin Name ... =end Name: fold from end of =begin line to start of =end line
|
|
46
|
+
function foldBeginEnd(state, lineStart) {
|
|
47
|
+
const line = state.doc.lineAt(lineStart);
|
|
48
|
+
const blockName = parseBeginLine(line.text);
|
|
49
|
+
if (!blockName)
|
|
50
|
+
return null;
|
|
51
|
+
// Search forward for matching =end
|
|
52
|
+
for (let i = line.number + 1; i <= state.doc.lines; i++) {
|
|
53
|
+
const candidate = state.doc.line(i);
|
|
54
|
+
const m = candidate.text.match(endRe);
|
|
55
|
+
if (m && m[2] === blockName) {
|
|
56
|
+
// Fold from end of =begin line to start of =end line
|
|
57
|
+
if (line.to >= candidate.from)
|
|
58
|
+
return null;
|
|
59
|
+
return { from: line.to, to: candidate.from };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Fold =headN: from end of heading line to start of next heading of same or higher level (or EOF)
|
|
65
|
+
function foldHeadSection(state, lineStart) {
|
|
66
|
+
const line = state.doc.lineAt(lineStart);
|
|
67
|
+
const m = line.text.match(headRe);
|
|
68
|
+
if (!m)
|
|
69
|
+
return null;
|
|
70
|
+
const level = parseInt(m[2] ?? '1', 10);
|
|
71
|
+
// Search forward for next heading of same or higher level
|
|
72
|
+
let foldEnd = state.doc.length;
|
|
73
|
+
for (let i = line.number + 1; i <= state.doc.lines; i++) {
|
|
74
|
+
const candidate = state.doc.line(i);
|
|
75
|
+
const hm = candidate.text.match(headRe);
|
|
76
|
+
if (hm) {
|
|
77
|
+
const candidateLevel = parseInt(hm[2] ?? '1', 10);
|
|
78
|
+
if (candidateLevel <= level) {
|
|
79
|
+
foldEnd = candidate.from;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Nothing to fold if heading is last line or only whitespace follows
|
|
85
|
+
if (line.to >= foldEnd)
|
|
86
|
+
return null;
|
|
87
|
+
// Trim trailing blank lines from fold range
|
|
88
|
+
let endPos = foldEnd;
|
|
89
|
+
while (endPos > line.to) {
|
|
90
|
+
const prevLine = state.doc.lineAt(endPos - 1);
|
|
91
|
+
if (prevLine.text.trim() !== '') {
|
|
92
|
+
endPos = prevLine.to;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
endPos = prevLine.from;
|
|
96
|
+
}
|
|
97
|
+
if (line.to >= endPos)
|
|
98
|
+
return null;
|
|
99
|
+
return { from: line.to, to: endPos };
|
|
100
|
+
}
|
|
101
|
+
export const podliteFoldService = foldService.of((state, lineStart) => {
|
|
102
|
+
const line = state.doc.lineAt(lineStart);
|
|
103
|
+
const verbatimRanges = getVerbatimRanges(state);
|
|
104
|
+
if (isInsideVerbatim(line.number, verbatimRanges))
|
|
105
|
+
return null;
|
|
106
|
+
return foldBeginEnd(state, lineStart) ?? foldHeadSection(state, lineStart);
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=foldPodlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"foldPodlite.js","sourceRoot":"","sources":["../src/foldPodlite.ts"],"names":[],"mappings":"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"}
|
package/esm/index.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ import HighlightedCode from './HighlightedCode';
|
|
|
4
4
|
export { Editor2 as Editor2 };
|
|
5
5
|
export { WindowWrapper as WindowWrapper };
|
|
6
6
|
export { HighlightedCode as HighlightedCode };
|
|
7
|
+
export type { EditorSessionState } from './types';
|
|
8
|
+
export type { ConverterResult, IPodliteEditor, PodliteEditorRef } from './Editor';
|
|
7
9
|
export default Editor2;
|
package/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,UAAU,CAAA;AAC9B,OAAO,aAAa,MAAM,WAAW,CAAA;AACrC,OAAO,eAAe,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,CAAA;AAC7B,OAAO,EAAE,aAAa,IAAI,aAAa,EAAE,CAAA;AACzC,OAAO,EAAE,eAAe,IAAI,eAAe,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,UAAU,CAAA;AAC9B,OAAO,aAAa,MAAM,WAAW,CAAA;AACrC,OAAO,eAAe,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,CAAA;AAC7B,OAAO,EAAE,aAAa,IAAI,aAAa,EAAE,CAAA;AACzC,OAAO,EAAE,eAAe,IAAI,eAAe,EAAE,CAAA;AAG7C,eAAe,OAAO,CAAA"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { keymap } from '@codemirror/view';
|
|
2
|
+
// Regex: =item with optional level digit(s) + separator
|
|
3
|
+
const itemRe = /^(\s*=item)(\d*)([ \t])/;
|
|
4
|
+
// Detect list type prefix at start of content
|
|
5
|
+
const prefixRe = /^(\[ \]|\[x\]|\[X\]|#)\s*/;
|
|
6
|
+
// Item is "empty" if content is nothing or only a type prefix
|
|
7
|
+
const emptyContentRe = /^(\[ \]|\[x\]|\[X\]|#)?\s*$/;
|
|
8
|
+
// Parse, Don't Validate — returns typed structure or null
|
|
9
|
+
function parseItemLine(lineText, lineFrom) {
|
|
10
|
+
const m = lineText.match(itemRe);
|
|
11
|
+
if (!m)
|
|
12
|
+
return null;
|
|
13
|
+
const prefix = m[1] ?? ''; // "\s*=item"
|
|
14
|
+
const digits = m[2] ?? ''; // level digits, may be empty
|
|
15
|
+
const sep = m[3] ?? ' '; // separator char
|
|
16
|
+
const indent = prefix.slice(0, prefix.length - 5); // whitespace before "=item"
|
|
17
|
+
const level = digits === '' ? 1 : parseInt(digits, 10);
|
|
18
|
+
const marker = `${indent}=item${digits}`;
|
|
19
|
+
return {
|
|
20
|
+
indent,
|
|
21
|
+
marker,
|
|
22
|
+
level,
|
|
23
|
+
separator: sep,
|
|
24
|
+
markerEnd: lineFrom + prefix.length + digits.length + sep.length,
|
|
25
|
+
content: lineText.slice(prefix.length + digits.length + sep.length),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Get the prefix to insert for the new item (checked → unchecked, # → #, none → '')
|
|
29
|
+
function resolveNewPrefix(content) {
|
|
30
|
+
const m = content.trim().match(prefixRe);
|
|
31
|
+
if (!m)
|
|
32
|
+
return '';
|
|
33
|
+
const prefix = m[1] ?? '';
|
|
34
|
+
if (prefix === '[x]' || prefix === '[X]' || prefix === '[ ]')
|
|
35
|
+
return '[ ] ';
|
|
36
|
+
if (prefix === '#')
|
|
37
|
+
return '# ';
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
// ─── List Continuation on Enter ────────────────────────────────────────────
|
|
41
|
+
export const listContinuationKeymap = keymap.of([
|
|
42
|
+
{
|
|
43
|
+
key: 'Enter',
|
|
44
|
+
run: (view) => {
|
|
45
|
+
const { state } = view;
|
|
46
|
+
const { head, from, to } = state.selection.main;
|
|
47
|
+
// Only single cursor (no selection)
|
|
48
|
+
if (from !== to)
|
|
49
|
+
return false;
|
|
50
|
+
const line = state.doc.lineAt(head);
|
|
51
|
+
const item = parseItemLine(line.text, line.from);
|
|
52
|
+
if (!item)
|
|
53
|
+
return false;
|
|
54
|
+
// Cursor inside marker → default
|
|
55
|
+
if (head < item.markerEnd)
|
|
56
|
+
return false;
|
|
57
|
+
const trimmedContent = item.content.trim();
|
|
58
|
+
// Empty item (including prefix-only like "[ ]" or "#") → remove marker, exit list
|
|
59
|
+
if (emptyContentRe.test(trimmedContent)) {
|
|
60
|
+
view.dispatch({
|
|
61
|
+
changes: { from: line.from, to: line.to, insert: '' },
|
|
62
|
+
selection: { anchor: line.from },
|
|
63
|
+
});
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
// Determine type prefix for new item
|
|
67
|
+
const newPrefix = resolveNewPrefix(trimmedContent);
|
|
68
|
+
// Split: text before cursor stays, text after → new =itemN [prefix]
|
|
69
|
+
const textAfterCursor = line.text.slice(head - line.from);
|
|
70
|
+
const newItem = `\n${item.marker} ${newPrefix}`;
|
|
71
|
+
view.dispatch({
|
|
72
|
+
changes: {
|
|
73
|
+
from: head,
|
|
74
|
+
to: line.to,
|
|
75
|
+
insert: newItem + textAfterCursor.trimStart(),
|
|
76
|
+
},
|
|
77
|
+
selection: {
|
|
78
|
+
anchor: head + newItem.length,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
// ─── Tab / Shift-Tab: change item level ────────────────────────────────────
|
|
86
|
+
const maxItemLevel = 6;
|
|
87
|
+
const minItemLevel = 1;
|
|
88
|
+
const indentPerLevel = ' '; // 2 spaces per level
|
|
89
|
+
function changeItemLevel(view, delta) {
|
|
90
|
+
const { state } = view;
|
|
91
|
+
const { from, to } = state.selection.main;
|
|
92
|
+
const startLine = state.doc.lineAt(from);
|
|
93
|
+
const endLine = state.doc.lineAt(to);
|
|
94
|
+
const changes = [];
|
|
95
|
+
for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) {
|
|
96
|
+
const line = state.doc.line(lineNum);
|
|
97
|
+
const item = parseItemLine(line.text, line.from);
|
|
98
|
+
if (!item)
|
|
99
|
+
continue;
|
|
100
|
+
const newLevel = item.level + delta;
|
|
101
|
+
if (newLevel < minItemLevel || newLevel > maxItemLevel)
|
|
102
|
+
continue;
|
|
103
|
+
// Adjust indentation: each level = 4 spaces
|
|
104
|
+
const newIndent = delta === 1 ? item.indent + indentPerLevel : item.indent.slice(indentPerLevel.length);
|
|
105
|
+
const oldMarkerLen = item.markerEnd - line.from;
|
|
106
|
+
const newMarker = `${newIndent}=item${newLevel}${item.separator}`;
|
|
107
|
+
changes.push({ from: line.from, to: line.from + oldMarkerLen, insert: newMarker });
|
|
108
|
+
}
|
|
109
|
+
if (changes.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
view.dispatch({ changes });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
export const itemLevelKeymap = keymap.of([
|
|
115
|
+
{
|
|
116
|
+
key: 'Tab',
|
|
117
|
+
run: (view) => changeItemLevel(view, 1),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'Shift-Tab',
|
|
121
|
+
run: (view) => changeItemLevel(view, -1),
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
//# sourceMappingURL=listContinuation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listContinuation.js","sourceRoot":"","sources":["../src/listContinuation.ts"],"names":[],"mappings":"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"}
|
package/esm/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare type FoldRange = {
|
|
2
|
+
readonly from: number;
|
|
3
|
+
readonly to: number;
|
|
4
|
+
};
|
|
5
|
+
export declare type EditorSessionState = {
|
|
6
|
+
readonly cursorOffset?: number;
|
|
7
|
+
readonly scrollTop?: number;
|
|
8
|
+
readonly foldedRanges?: ReadonlyArray<FoldRange>;
|
|
9
|
+
readonly isPreviewMode?: boolean;
|
|
10
|
+
readonly isHalfPreviewMode?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
package/esm/types.js
ADDED
package/esm/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/lib/Editor.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ViewUpdate, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
|
3
|
+
import type { EditorSessionState } from './types';
|
|
3
4
|
export interface ConverterResult {
|
|
4
5
|
errors?: any;
|
|
5
6
|
result: JSX.Element | string;
|
|
@@ -27,6 +28,10 @@ export interface IPodliteEditor extends ReactCodeMirrorProps {
|
|
|
27
28
|
onOpenLink?: (url: string) => void;
|
|
28
29
|
/** Enable code highlighting */
|
|
29
30
|
enableHighlighting?: boolean;
|
|
31
|
+
/** Initial editor session state to restore (cursor, scroll, folds) */
|
|
32
|
+
initialEditorState?: EditorSessionState;
|
|
33
|
+
/** Called when editor state changes (cursor move, scroll, fold/unfold) */
|
|
34
|
+
onEditorStateChange?: (state: EditorSessionState) => void;
|
|
30
35
|
}
|
|
31
36
|
export interface PodliteEditorRef {
|
|
32
37
|
editor: React.RefObject<ReactCodeMirrorRef>;
|
package/lib/Editor.js
CHANGED
|
@@ -10,9 +10,12 @@ const podlite_2 = require("./podlite");
|
|
|
10
10
|
const theme_1 = require("./theme");
|
|
11
11
|
const view_1 = require("@codemirror/view");
|
|
12
12
|
const commands_1 = require("@codemirror/commands");
|
|
13
|
+
const language_1 = require("@codemirror/language");
|
|
13
14
|
const react_is_1 = require("react-is");
|
|
14
15
|
const autocomplete_1 = require("@codemirror/autocomplete");
|
|
15
16
|
const dict_1 = (0, tslib_1.__importDefault)(require("./dict"));
|
|
17
|
+
const listContinuation_1 = require("./listContinuation");
|
|
18
|
+
const foldPodlite_1 = require("./foldPodlite");
|
|
16
19
|
const HighlightedCode_1 = (0, tslib_1.__importDefault)(require("./HighlightedCode"));
|
|
17
20
|
function useDebouncedEffect(fn, deps, time) {
|
|
18
21
|
const dependencies = [...deps, time];
|
|
@@ -30,10 +33,10 @@ function PodliteEditorInternal(props, ref) {
|
|
|
30
33
|
if (!isBrowser)
|
|
31
34
|
return null;
|
|
32
35
|
const { onChange, enableScroll = true, enablePreview = true, extensions = [], previewWidth = '50%', onPreviewMode, isFullscreen, makePreviewComponent, startLinePreview = 1, // start line number
|
|
33
|
-
enableAutocompletion = false, onOpenLink, enableHighlighting = false, ...codemirrorProps } = props;
|
|
36
|
+
enableAutocompletion = false, onOpenLink, enableHighlighting = false, initialEditorState, onEditorStateChange, ...codemirrorProps } = props;
|
|
34
37
|
const full_preview = previewWidth === '100%';
|
|
35
38
|
const [value, setValue] = (0, react_1.useState)(props.value || '');
|
|
36
|
-
const
|
|
39
|
+
const pendingTextRef = (0, react_1.useRef)(props.value || '');
|
|
37
40
|
const codeMirror = (0, react_1.useRef)(null);
|
|
38
41
|
const container = (0, react_1.useRef)(null);
|
|
39
42
|
const containerEditor = (0, react_1.useRef)(null);
|
|
@@ -41,10 +44,93 @@ function PodliteEditorInternal(props, ref) {
|
|
|
41
44
|
const active = (0, react_1.useRef)(full_preview ? 'preview' : 'editor');
|
|
42
45
|
const topLine = (0, react_1.useRef)(startLinePreview);
|
|
43
46
|
const $viewHeight = (0, react_1.useRef)(0);
|
|
47
|
+
const initialStateApplied = (0, react_1.useRef)(false);
|
|
48
|
+
// Stable refs for callbacks used in memoized extensions
|
|
49
|
+
// Stable refs for callbacks used in memoized extensions
|
|
50
|
+
const onEditorStateChangeRef = (0, react_1.useRef)(onEditorStateChange);
|
|
51
|
+
onEditorStateChangeRef.current = onEditorStateChange;
|
|
52
|
+
const onOpenLinkRef = (0, react_1.useRef)(onOpenLink);
|
|
53
|
+
onOpenLinkRef.current = onOpenLink;
|
|
54
|
+
// Stable basicSetup config to prevent CodeMirror reconfiguration on re-render
|
|
55
|
+
const basicSetupConfig = react_1.default.useMemo(() => ({ defaultKeymap: false }), []);
|
|
44
56
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
45
57
|
editor: codeMirror,
|
|
46
58
|
preview: preview,
|
|
47
59
|
}), [codeMirror]);
|
|
60
|
+
// Helper: serialize current fold ranges from EditorState
|
|
61
|
+
const serializeFoldedRanges = (0, react_1.useCallback)((view) => {
|
|
62
|
+
const ranges = [];
|
|
63
|
+
const folded = (0, language_1.foldedRanges)(view.state);
|
|
64
|
+
const iter = folded.iter();
|
|
65
|
+
while (iter.value) {
|
|
66
|
+
ranges.push({ from: iter.from, to: iter.to });
|
|
67
|
+
iter.next();
|
|
68
|
+
}
|
|
69
|
+
return ranges;
|
|
70
|
+
}, []);
|
|
71
|
+
// Reset applied flag when initialEditorState changes (new file opened)
|
|
72
|
+
(0, react_1.useEffect)(() => {
|
|
73
|
+
initialStateApplied.current = false;
|
|
74
|
+
}, [initialEditorState]);
|
|
75
|
+
// Restore editor session state once after mount or file change
|
|
76
|
+
(0, react_1.useEffect)(() => {
|
|
77
|
+
if (initialStateApplied.current || !initialEditorState)
|
|
78
|
+
return;
|
|
79
|
+
const view = codeMirror.current?.view;
|
|
80
|
+
if (!view)
|
|
81
|
+
return;
|
|
82
|
+
initialStateApplied.current = true;
|
|
83
|
+
// Restore cursor position
|
|
84
|
+
if (initialEditorState.cursorOffset != null) {
|
|
85
|
+
const offset = Math.min(initialEditorState.cursorOffset, view.state.doc.length);
|
|
86
|
+
view.dispatch({ selection: { anchor: offset } });
|
|
87
|
+
}
|
|
88
|
+
// Restore fold ranges
|
|
89
|
+
if (initialEditorState.foldedRanges && initialEditorState.foldedRanges.length > 0) {
|
|
90
|
+
const effects = initialEditorState.foldedRanges
|
|
91
|
+
.filter(r => r.from < view.state.doc.length && r.to <= view.state.doc.length)
|
|
92
|
+
.map(r => language_1.foldEffect.of({ from: r.from, to: r.to }));
|
|
93
|
+
if (effects.length > 0) {
|
|
94
|
+
view.dispatch({ effects });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Restore scroll position (after folds applied, so layout is correct)
|
|
98
|
+
// and focus the editor so the blinking cursor is visible
|
|
99
|
+
requestAnimationFrame(() => {
|
|
100
|
+
if (initialEditorState.scrollTop != null) {
|
|
101
|
+
view.scrollDOM.scrollTop = initialEditorState.scrollTop;
|
|
102
|
+
}
|
|
103
|
+
view.focus();
|
|
104
|
+
});
|
|
105
|
+
}, [initialEditorState, codeMirror.current?.view]);
|
|
106
|
+
// Expose editor state changes via callback
|
|
107
|
+
(0, react_1.useEffect)(() => {
|
|
108
|
+
if (!onEditorStateChange)
|
|
109
|
+
return;
|
|
110
|
+
const view = codeMirror.current?.view;
|
|
111
|
+
if (!view)
|
|
112
|
+
return;
|
|
113
|
+
const emitState = () => {
|
|
114
|
+
onEditorStateChange({
|
|
115
|
+
cursorOffset: view.state.selection.main.head,
|
|
116
|
+
scrollTop: view.scrollDOM.scrollTop,
|
|
117
|
+
foldedRanges: serializeFoldedRanges(view),
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
// Debounce scroll events
|
|
121
|
+
let scrollTimer = null;
|
|
122
|
+
const handleScroll = () => {
|
|
123
|
+
if (scrollTimer)
|
|
124
|
+
clearTimeout(scrollTimer);
|
|
125
|
+
scrollTimer = setTimeout(emitState, 200);
|
|
126
|
+
};
|
|
127
|
+
view.scrollDOM.addEventListener('scroll', handleScroll);
|
|
128
|
+
return () => {
|
|
129
|
+
view.scrollDOM.removeEventListener('scroll', handleScroll);
|
|
130
|
+
if (scrollTimer)
|
|
131
|
+
clearTimeout(scrollTimer);
|
|
132
|
+
};
|
|
133
|
+
}, [onEditorStateChange, codeMirror.current?.view, serializeFoldedRanges]);
|
|
48
134
|
const height = isFullscreen
|
|
49
135
|
? '100%'
|
|
50
136
|
: typeof codemirrorProps.height === 'number'
|
|
@@ -52,7 +138,9 @@ function PodliteEditorInternal(props, ref) {
|
|
|
52
138
|
: codemirrorProps.height;
|
|
53
139
|
const preValue = props.value;
|
|
54
140
|
(0, react_1.useEffect)(() => setValue(preValue ?? ''), [preValue]);
|
|
55
|
-
(0, react_1.useEffect)(() =>
|
|
141
|
+
(0, react_1.useEffect)(() => {
|
|
142
|
+
pendingTextRef.current = preValue ?? '';
|
|
143
|
+
}, [preValue]);
|
|
56
144
|
const fullRef = (0, react_1.useRef)(isFullscreen);
|
|
57
145
|
(0, react_1.useEffect)(() => {
|
|
58
146
|
fullRef.current = isFullscreen;
|
|
@@ -207,8 +295,9 @@ function PodliteEditorInternal(props, ref) {
|
|
|
207
295
|
},
|
|
208
296
|
...commands_1.defaultKeymap,
|
|
209
297
|
]);
|
|
210
|
-
//
|
|
211
|
-
|
|
298
|
+
// Memoized extensions to prevent CodeMirror reconfiguration on re-render
|
|
299
|
+
// (reconfiguration closes panels like search)
|
|
300
|
+
const modKeyClass = react_1.default.useMemo(() => view_1.EditorView.domEventHandlers({
|
|
212
301
|
keydown(e, view) {
|
|
213
302
|
if (e.metaKey || e.ctrlKey)
|
|
214
303
|
view.dom.classList.add('cm-mod-pressed');
|
|
@@ -220,12 +309,11 @@ function PodliteEditorInternal(props, ref) {
|
|
|
220
309
|
blur(_, view) {
|
|
221
310
|
view.dom.classList.remove('cm-mod-pressed');
|
|
222
311
|
},
|
|
223
|
-
// defensive: if mouse leaves, clear the class (helps when key is released elsewhere)
|
|
224
312
|
mouseleave(_, view) {
|
|
225
313
|
view.dom.classList.remove('cm-mod-pressed');
|
|
226
314
|
},
|
|
227
|
-
});
|
|
228
|
-
const openStyledLinkOnClick = view_1.EditorView.domEventHandlers({
|
|
315
|
+
}), []);
|
|
316
|
+
const openStyledLinkOnClick = react_1.default.useMemo(() => view_1.EditorView.domEventHandlers({
|
|
229
317
|
click(event, view) {
|
|
230
318
|
const target = event.target;
|
|
231
319
|
if (!target)
|
|
@@ -233,35 +321,42 @@ function PodliteEditorInternal(props, ref) {
|
|
|
233
321
|
const el = target.closest('.cm-clickable-link');
|
|
234
322
|
if (!el)
|
|
235
323
|
return;
|
|
236
|
-
// require Cmd/Ctrl for activation
|
|
237
324
|
const modPressed = event.metaKey || event.ctrlKey;
|
|
238
325
|
if (!modPressed)
|
|
239
326
|
return;
|
|
240
327
|
const from = view.posAtDOM(el, 0);
|
|
241
328
|
const to = view.posAtDOM(el, el.childNodes.length);
|
|
242
329
|
const text = view.state.doc.sliceString(from, to).trim();
|
|
243
|
-
|
|
244
|
-
onOpenLink(text); // i.e window.open(text, '_blank', 'noopener')
|
|
245
|
-
}
|
|
330
|
+
onOpenLinkRef.current?.(text);
|
|
246
331
|
event.preventDefault();
|
|
247
332
|
event.stopPropagation();
|
|
248
333
|
},
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
334
|
+
}), []);
|
|
335
|
+
// Track fold/unfold and cursor changes to emit editor state
|
|
336
|
+
const stateChangeListener = react_1.default.useMemo(() => onEditorStateChange
|
|
337
|
+
? view_1.EditorView.updateListener.of(update => {
|
|
338
|
+
const hasFoldChange = update.transactions.some(tr => tr.effects.some(e => e.is(language_1.foldEffect) || e.is(language_1.unfoldEffect)));
|
|
339
|
+
const hasCursorChange = update.selectionSet;
|
|
340
|
+
if (hasFoldChange || hasCursorChange) {
|
|
341
|
+
onEditorStateChangeRef.current?.({
|
|
342
|
+
cursorOffset: update.state.selection.main.head,
|
|
343
|
+
scrollTop: update.view.scrollDOM.scrollTop,
|
|
344
|
+
foldedRanges: serializeFoldedRanges(update.view),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
: [], [!!onEditorStateChange, serializeFoldedRanges]);
|
|
349
|
+
const linkTheme = react_1.default.useMemo(() => view_1.EditorView.theme({
|
|
350
|
+
'&.cm-editor.cm-mod-pressed .cm-clickable-link:hover': {
|
|
351
|
+
cursor: 'pointer',
|
|
352
|
+
},
|
|
353
|
+
}), []);
|
|
354
|
+
const makeApply = (0, react_1.useCallback)((text) => (editor, completion, from, to) => {
|
|
262
355
|
return (0, autocomplete_1.snippet)(text)(editor, completion, from - 1, to);
|
|
263
|
-
};
|
|
264
|
-
|
|
356
|
+
}, []);
|
|
357
|
+
const autocompletionExt = react_1.default.useMemo(() => {
|
|
358
|
+
if (!enableAutocompletion)
|
|
359
|
+
return null;
|
|
265
360
|
const langDict = dict_1.default.filter(({ lang = 'pod6' }) => lang === 'pod6');
|
|
266
361
|
const completions = langDict.map(({ displayText, text }) => {
|
|
267
362
|
function cleanBraces(text) {
|
|
@@ -276,8 +371,6 @@ function PodliteEditorInternal(props, ref) {
|
|
|
276
371
|
});
|
|
277
372
|
function myCompletions(context) {
|
|
278
373
|
let before = context.matchBefore(/^\s*=\w*/);
|
|
279
|
-
// If completion wasn't explicitly started and there
|
|
280
|
-
// is no word before the cursor, don't open completions.
|
|
281
374
|
if (!context.explicit && !before)
|
|
282
375
|
return null;
|
|
283
376
|
return {
|
|
@@ -286,11 +379,50 @@ function PodliteEditorInternal(props, ref) {
|
|
|
286
379
|
validFor: /^=\w*$/,
|
|
287
380
|
};
|
|
288
381
|
}
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
382
|
+
return (0, autocomplete_1.autocompletion)({ override: [myCompletions] });
|
|
383
|
+
}, [enableAutocompletion, makeApply]);
|
|
384
|
+
const extensionsData = react_1.default.useMemo(() => {
|
|
385
|
+
const exts = [
|
|
386
|
+
(0, podlite_2.podliteLang)(),
|
|
387
|
+
view_1.EditorView.lineWrapping,
|
|
388
|
+
foldPodlite_1.podliteFoldService,
|
|
389
|
+
(0, language_1.foldGutter)(),
|
|
390
|
+
view_1.keymap.of(language_1.foldKeymap),
|
|
391
|
+
stateChangeListener,
|
|
392
|
+
listContinuation_1.itemLevelKeymap,
|
|
393
|
+
listContinuation_1.listContinuationKeymap,
|
|
394
|
+
preventToggleComment,
|
|
395
|
+
];
|
|
396
|
+
if (onOpenLink) {
|
|
397
|
+
exts.push(modKeyClass, openStyledLinkOnClick, linkTheme);
|
|
398
|
+
}
|
|
399
|
+
if (enableScroll) {
|
|
400
|
+
exts.push(scrollExtensions);
|
|
401
|
+
}
|
|
402
|
+
if (autocompletionExt) {
|
|
403
|
+
exts.push(autocompletionExt);
|
|
404
|
+
}
|
|
405
|
+
return exts;
|
|
406
|
+
}, [
|
|
407
|
+
stateChangeListener,
|
|
408
|
+
modKeyClass,
|
|
409
|
+
openStyledLinkOnClick,
|
|
410
|
+
linkTheme,
|
|
411
|
+
!!onOpenLink,
|
|
412
|
+
enableScroll,
|
|
413
|
+
scrollExtensions,
|
|
414
|
+
preventToggleComment,
|
|
415
|
+
autocompletionExt,
|
|
416
|
+
]);
|
|
417
|
+
// Debounced preview update: check ref every 300ms, update value state only when text changed
|
|
418
|
+
(0, react_1.useEffect)(() => {
|
|
419
|
+
const interval = setInterval(() => {
|
|
420
|
+
if (pendingTextRef.current !== value) {
|
|
421
|
+
setValue(pendingTextRef.current);
|
|
422
|
+
}
|
|
423
|
+
}, 300);
|
|
424
|
+
return () => clearInterval(interval);
|
|
425
|
+
}, [value]);
|
|
294
426
|
(0, react_1.useEffect)(() => {
|
|
295
427
|
if (!isBrowser || !container.current)
|
|
296
428
|
return;
|
|
@@ -328,10 +460,14 @@ function PodliteEditorInternal(props, ref) {
|
|
|
328
460
|
}, 100);
|
|
329
461
|
return () => clearTimeout(timer);
|
|
330
462
|
}, [startLinePreview, enablePreview]); // Only run when startLinePreview or enablePreview changes
|
|
331
|
-
const handleChange = (value, viewUpdate) => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
463
|
+
const handleChange = (0, react_1.useCallback)((value, viewUpdate) => {
|
|
464
|
+
pendingTextRef.current = value;
|
|
465
|
+
// Defer parent callback to next frame so CodeMirror textupdate handler
|
|
466
|
+
// completes immediately and the typed character appears without delay
|
|
467
|
+
requestAnimationFrame(() => {
|
|
468
|
+
onChange?.(value, viewUpdate);
|
|
469
|
+
});
|
|
470
|
+
}, [onChange]);
|
|
335
471
|
(0, react_1.useEffect)(() => {
|
|
336
472
|
if (preview.current) {
|
|
337
473
|
const $preview = preview.current;
|
|
@@ -482,7 +618,7 @@ function PodliteEditorInternal(props, ref) {
|
|
|
482
618
|
};
|
|
483
619
|
const conentView = (react_1.default.createElement("div", { className: `podlite-editor-content`, style: { height: codemirrorProps?.height } },
|
|
484
620
|
react_1.default.createElement("div", { className: `podlite-editor-content-editor`, ref: containerEditor },
|
|
485
|
-
react_1.default.createElement(react_codemirror_1.default, { theme: theme_1.defaultTheme,
|
|
621
|
+
react_1.default.createElement(react_codemirror_1.default, { theme: theme_1.defaultTheme, indentWithTab: false, autoFocus: true, ...codemirrorProps, basicSetup: basicSetupConfig, className: `podlite-editor-inner`, extensions: extensionsData, height: height, ref: codeMirror, onChange: handleChange })),
|
|
486
622
|
enablePreview && (react_1.default.createElement("div", { className: clsPreview, style: { overflow: full_preview ? 'visible' : 'hidden' }, ref: preview }, previewContent()))));
|
|
487
623
|
return (react_1.default.createElement("div", null,
|
|
488
624
|
react_1.default.createElement("div", { className: cls, ref: container }, conentView),
|