Repository navigation
Expand file tree
/
Copy pathshared-editor.js
More file actions
158 lines (144 loc) · 5.71 KB
/
Copy pathshared-editor.js
File metadata and controls
158 lines (144 loc) · 5.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* Reusable Python editor foundation for Code Explorer workspaces.
*
* The existing execution workspace has trace-specific CodeMirror extensions.
* This smaller controller supplies common source, wrapping, sizing, and
* fallback behavior to the DSA workspace. Trace playback and read-only learning
* comments remain owned by the DSA controller rather than the editor document.
*/
/** Pinned CodeMirror dependency URLs audited and already used by Code Explorer. */
const CODEMIRROR_MODULES = Object.freeze({
editor: "https://esm.sh/codemirror@6.0.2?deps=@codemirror/state@6.7.1,@codemirror/view@6.43.6,@codemirror/language@6.12.4,@lezer/highlight@1.2.3",
python: "https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.7.1,@codemirror/view@6.43.6,@codemirror/language@6.12.4,@lezer/highlight@1.2.3",
language: "https://esm.sh/@codemirror/language@6.12.4?deps=@codemirror/state@6.7.1,@codemirror/view@6.43.6,@lezer/highlight@1.2.3",
highlight: "https://esm.sh/@lezer/highlight@1.2.3",
state: "https://esm.sh/@codemirror/state@6.7.1",
});
/** Font sizes exposed by both workspace toolbars. */
export const EDITOR_FONT_SIZES = Object.freeze([12, 14, 16, 18, 20, 22]);
/**
* Creates a CodeMirror Python editor with a native textarea fallback.
* @param {{
* mount: HTMLElement,
* shell: HTMLElement,
* initialCode: string,
* preferences: {wrap: boolean, fontSize: number},
* onChange: (code: string) => void,
* onFallback?: (error: unknown) => void
* }} options Required DOM, source, preference, and callback values.
* @returns {Promise<{
* getCode: () => string,
* setCode: (code: string) => void,
* focus: () => void,
* applyPreferences: (preferences: {wrap: boolean, fontSize: number}) => void,
* enhanced: boolean
* }>} Stable editor API independent from the mounted implementation.
*/
export async function createPythonEditor(options) {
const { mount, shell, initialCode, onChange, onFallback = () => {} } = options;
let code = initialCode;
let editorView = null;
let fallbackEditor = null;
let editorConfiguration = null;
/**
* Applies validated display preferences without changing Python source.
* @param {{wrap: boolean, fontSize: number}} preferences Current presentation choices.
*/
function applyPreferences(preferences) {
const safeFontSize = EDITOR_FONT_SIZES.includes(Number(preferences.fontSize))
? Number(preferences.fontSize)
: 14;
shell.style.setProperty("--editor-font-size", `${safeFontSize}px`);
if (fallbackEditor) {
fallbackEditor.wrap = preferences.wrap ? "soft" : "off";
fallbackEditor.classList.toggle("wrap-disabled", !preferences.wrap);
}
if (editorView && editorConfiguration) {
editorView.dispatch({
effects: editorConfiguration.wrapCompartment.reconfigure(
preferences.wrap ? editorConfiguration.lineWrapping : [],
),
});
editorView.requestMeasure();
}
}
try {
// CodeMirror packages share exact state and highlighting versions so token
// identity remains stable across independently delivered ES modules.
const [
{ basicSetup, EditorView },
{ python },
{ syntaxHighlighting },
{ classHighlighter },
{ Compartment },
] = await Promise.all([
import(CODEMIRROR_MODULES.editor),
import(CODEMIRROR_MODULES.python),
import(CODEMIRROR_MODULES.language),
import(CODEMIRROR_MODULES.highlight),
import(CODEMIRROR_MODULES.state),
]);
// A compartment changes wrapping without rebuilding the document or losing selection.
const wrapCompartment = new Compartment();
editorConfiguration = { wrapCompartment, lineWrapping: EditorView.lineWrapping };
editorView = new EditorView({
doc: code,
extensions: [
basicSetup,
python(),
syntaxHighlighting(classHighlighter),
wrapCompartment.of(options.preferences.wrap ? EditorView.lineWrapping : []),
EditorView.updateListener.of((update) => {
if (!update.docChanged) return;
code = update.state.doc.toString();
onChange(code);
}),
],
parent: mount,
});
} catch (error) {
// A basic editor preserves all source editing when the optional CDN modules fail.
console.error("Code Explorer enhanced editor initialization failed.", error);
fallbackEditor = document.createElement("textarea");
fallbackEditor.className = "fallback-editor";
fallbackEditor.value = code;
fallbackEditor.spellcheck = false;
fallbackEditor.setAttribute("aria-label", "Python code editor");
fallbackEditor.addEventListener("input", () => {
code = fallbackEditor.value;
onChange(code);
});
mount.replaceChildren(fallbackEditor);
onFallback(error);
}
applyPreferences(options.preferences);
return {
/** @returns {string} Complete current source from the active editor. */
getCode: () => editorView?.state.doc.toString() ?? fallbackEditor?.value ?? code,
/**
* Replaces the complete document after an explicit caller-owned action.
* @param {string} nextCode Complete Python source to display.
*/
setCode: (nextCode) => {
code = nextCode;
if (editorView) {
editorView.dispatch({
changes: { from: 0, to: editorView.state.doc.length, insert: nextCode },
selection: { anchor: 0 },
});
} else if (fallbackEditor) {
fallbackEditor.value = nextCode;
onChange(nextCode);
} else {
onChange(nextCode);
}
},
/** Returns keyboard focus to the active editing surface. */
focus: () => {
if (editorView) editorView.focus();
else fallbackEditor?.focus();
},
applyPreferences,
enhanced: Boolean(editorView),
};
}