munchboka-edutools 0.1.0__py3-none-any.whl
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.
- munchboka_edutools/__init__.py +182 -0
- munchboka_edutools/_plotmath_shim.py +126 -0
- munchboka_edutools/_version.py +2 -0
- munchboka_edutools/directives/__init__.py +1 -0
- munchboka_edutools/directives/admonitions.py +356 -0
- munchboka_edutools/directives/cas_popup.py +272 -0
- munchboka_edutools/directives/dialogue.py +137 -0
- munchboka_edutools/directives/escape_room.py +296 -0
- munchboka_edutools/directives/factor_tree.py +549 -0
- munchboka_edutools/directives/ggb.py +209 -0
- munchboka_edutools/directives/ggb_icon.py +62 -0
- munchboka_edutools/directives/ggb_popup.py +165 -0
- munchboka_edutools/directives/horner.py +324 -0
- munchboka_edutools/directives/interactive_code.py +75 -0
- munchboka_edutools/directives/jeopardy.py +252 -0
- munchboka_edutools/directives/multi_plot.py +1126 -0
- munchboka_edutools/directives/pair_puzzle.py +191 -0
- munchboka_edutools/directives/parsons.py +109 -0
- munchboka_edutools/directives/plot.py +3012 -0
- munchboka_edutools/directives/poly_icon.py +91 -0
- munchboka_edutools/directives/polydiv.py +344 -0
- munchboka_edutools/directives/quiz.py +291 -0
- munchboka_edutools/directives/signchart.py +474 -0
- munchboka_edutools/directives/timed_quiz.py +436 -0
- munchboka_edutools/directives/turtle.py +157 -0
- munchboka_edutools/static/css/admonitions.css +2012 -0
- munchboka_edutools/static/css/cas_popup.css +242 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_cm.css +112 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_default_cm.css +40 -0
- munchboka_edutools/static/css/code_mirror_themes/github_dark_high_contrast_cm.css +141 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_cm.css +120 -0
- munchboka_edutools/static/css/code_mirror_themes/github_light_default_cm.css +108 -0
- munchboka_edutools/static/css/code_mirror_themes/one_dark_cm.css +121 -0
- munchboka_edutools/static/css/dialogue.css +92 -0
- munchboka_edutools/static/css/escapeRoom/escape-room.css +223 -0
- munchboka_edutools/static/css/figures.css +274 -0
- munchboka_edutools/static/css/general_style.css +74 -0
- munchboka_edutools/static/css/github-dark-high-contrast.css +141 -0
- munchboka_edutools/static/css/github-dark.css +112 -0
- munchboka_edutools/static/css/github-light.css +120 -0
- munchboka_edutools/static/css/interactive_code/style.css +575 -0
- munchboka_edutools/static/css/interactive_code.css +582 -0
- munchboka_edutools/static/css/jeopardy.css +476 -0
- munchboka_edutools/static/css/pairPuzzle/style.css +177 -0
- munchboka_edutools/static/css/parsons/parsonsPuzzle.css +331 -0
- munchboka_edutools/static/css/quiz.css +312 -0
- munchboka_edutools/static/css/timedQuiz.css +375 -0
- munchboka_edutools/static/icons/ggb/mode_evaluate.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_extremum.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_intersect.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_nsolve.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_numeric.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_point.svg +1 -0
- munchboka_edutools/static/icons/ggb/mode_solve.svg +1 -0
- munchboka_edutools/static/icons/misc/windows-logo.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/dark_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/academic.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/chat_bubble.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/check.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/cmd_line.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/fire.svg +4 -0
- munchboka_edutools/static/icons/outline/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/magnifying.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/pencil_square.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/question.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/square_check.svg +1 -0
- munchboka_edutools/static/icons/outline/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/summary.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/undo.svg +3 -0
- munchboka_edutools/static/icons/outline/light_mode/unlock.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicdown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/cubicup.svg +3 -0
- munchboka_edutools/static/icons/polyicons/frown.svg +3 -0
- munchboka_edutools/static/icons/polyicons/smile.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/dark_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/dark_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/dark_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/dark_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/academic.svg +5 -0
- munchboka_edutools/static/icons/solid/light_mode/backward.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/book.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/brain.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/file.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/fire.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/key.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/pencil_square.svg +4 -0
- munchboka_edutools/static/icons/solid/light_mode/play.svg +3 -0
- munchboka_edutools/static/icons/solid/light_mode/python.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/scroll.svg +1 -0
- munchboka_edutools/static/icons/solid/light_mode/stop.svg +3 -0
- munchboka_edutools/static/icons/stickers/edit.svg +1 -0
- munchboka_edutools/static/icons/stickers/pencil_square.svg +3 -0
- munchboka_edutools/static/js/casThemeManager.js +99 -0
- munchboka_edutools/static/js/escapeRoom/escape-room.js +219 -0
- munchboka_edutools/static/js/geogebra-setup.js +6 -0
- munchboka_edutools/static/js/highlight-init.js +6 -0
- munchboka_edutools/static/js/interactiveCode/codeEditor.js +632 -0
- munchboka_edutools/static/js/interactiveCode/interactiveCodeSetup.js +348 -0
- munchboka_edutools/static/js/interactiveCode/pythonRunner.js +336 -0
- munchboka_edutools/static/js/interactiveCode/turtleCode.js +203 -0
- munchboka_edutools/static/js/interactiveCode/workerManager.js +353 -0
- munchboka_edutools/static/js/interactive_code/codeEditor.js +662 -0
- munchboka_edutools/static/js/interactive_code/interactiveCodeSetup.js +252 -0
- munchboka_edutools/static/js/interactive_code/pythonRunner.js +145 -0
- munchboka_edutools/static/js/interactive_code/turtleCode.js +56 -0
- munchboka_edutools/static/js/interactive_code/workerManager.js +204 -0
- munchboka_edutools/static/js/jeopardy.js +457 -0
- munchboka_edutools/static/js/pairPuzzle/draggableItem.js +64 -0
- munchboka_edutools/static/js/pairPuzzle/dropZone.js +119 -0
- munchboka_edutools/static/js/pairPuzzle/game.js +160 -0
- munchboka_edutools/static/js/parsons/parsonsPuzzle.js +641 -0
- munchboka_edutools/static/js/quiz.js +422 -0
- munchboka_edutools/static/js/skulpt/skulpt.js +35721 -0
- munchboka_edutools/static/js/timedQuiz/multipleChoiceQuestion.js +184 -0
- munchboka_edutools/static/js/timedQuiz/timedMultipleChoiceQuiz.js +244 -0
- munchboka_edutools/static/js/timedQuiz/utils.js +6 -0
- munchboka_edutools/static/js/utils.js +3 -0
- munchboka_edutools-0.1.0.dist-info/METADATA +107 -0
- munchboka_edutools-0.1.0.dist-info/RECORD +150 -0
- munchboka_edutools-0.1.0.dist-info/WHEEL +4 -0
- munchboka_edutools-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CodeEditor {
|
|
5
|
+
constructor(editorId) {
|
|
6
|
+
this.editorId = editorId; // ID of the HTML textarea element to convert to a code editor
|
|
7
|
+
this.loadAddons().then(() => {
|
|
8
|
+
this.editor = this.initializeEditor(editorId); // Initialize the CodeMirror editor instance
|
|
9
|
+
this.editor = this.addCommentOverlay(this.editor); // Add custom overlay for highlighting comments
|
|
10
|
+
this.setupThemeListener(); // Set up a listener to detect and apply theme changes
|
|
11
|
+
this.refreshOnVisibilityChange(); // Add this line
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// this.editor = this.initializeEditor(editorId); // Initialize the CodeMirror editor instance
|
|
15
|
+
// this.editor = this.addCommentOverlay(this.editor); // Add custom overlay for highlighting comments
|
|
16
|
+
// this.setupThemeListener(); // Set up a listener to detect and apply theme changes
|
|
17
|
+
// this.refreshOnVisibilityChange(); // Add this line
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initializes the CodeMirror editor with custom settings.
|
|
22
|
+
* @param {string} editorId - The ID of the textarea element to enhance with CodeMirror.
|
|
23
|
+
* @returns {Object} - The initialized CodeMirror editor instance.
|
|
24
|
+
*/
|
|
25
|
+
initializeEditor(editorId) {
|
|
26
|
+
const self = this;
|
|
27
|
+
const editor = CodeMirror.fromTextArea(document.getElementById(editorId), {
|
|
28
|
+
mode: {
|
|
29
|
+
name: "python", // Language mode set to Python
|
|
30
|
+
version: 3, // Python 3 syntax
|
|
31
|
+
},
|
|
32
|
+
lineNumbers: true, // Enable line numbers
|
|
33
|
+
theme: this.getCurrentTheme(), // Set the initial theme based on user preference
|
|
34
|
+
tabSize: 4, // Set the tab size for indentation
|
|
35
|
+
indentUnit: 4, // Number of spaces per indentation level
|
|
36
|
+
matchBrackets: true, // Highlight matching brackets
|
|
37
|
+
autoCloseBrackets: true, // Automatically close brackets
|
|
38
|
+
extraKeys: {
|
|
39
|
+
// Tab navigates placeholders if a snippet is active; otherwise inserts spaces
|
|
40
|
+
Tab: (cm) => {
|
|
41
|
+
const st = cm.state && cm.state.snippetPH;
|
|
42
|
+
if (st && Array.isArray(st.markers) && st.markers.length) {
|
|
43
|
+
// Try to move to the next existing marker
|
|
44
|
+
let i = st.index + 1;
|
|
45
|
+
let moved = false;
|
|
46
|
+
while (i < st.markers.length) {
|
|
47
|
+
const pos = st.markers[i] && st.markers[i].find();
|
|
48
|
+
if (pos) {
|
|
49
|
+
st.index = i;
|
|
50
|
+
cm.setSelection(pos.from, pos.to);
|
|
51
|
+
moved = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
if (!moved) {
|
|
57
|
+
// No further placeholders: clear and fall back to normal Tab
|
|
58
|
+
this.clearSnippetPlaceholders(cm);
|
|
59
|
+
this.replaceTabWithSpaces(cm);
|
|
60
|
+
}
|
|
61
|
+
return; // handled
|
|
62
|
+
}
|
|
63
|
+
this.replaceTabWithSpaces(cm);
|
|
64
|
+
},
|
|
65
|
+
"Enter": function(cm) {
|
|
66
|
+
// If a completion popup is open, let it handle Enter
|
|
67
|
+
if (cm.state && cm.state.completionActive) {
|
|
68
|
+
return CodeMirror.Pass;
|
|
69
|
+
}
|
|
70
|
+
// Try snippet expansion first
|
|
71
|
+
if (self.tryExpandSnippet(cm)) return;
|
|
72
|
+
|
|
73
|
+
var cursor = cm.getCursor();
|
|
74
|
+
var line = cm.getLine(cursor.line);
|
|
75
|
+
var currentIndent = line.match(/^\s*/)[0]; // Get current indentation level
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if (cursor.ch === 0) {
|
|
79
|
+
var currentLine = cm.getLine(cursor.line);
|
|
80
|
+
var currentIndent = currentLine.match(/^\s*/)[0];
|
|
81
|
+
cm.replaceSelection("\n" + currentIndent);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Rest of your existing logic
|
|
86
|
+
if (line.trim() === '') {
|
|
87
|
+
var nextLine = cursor.line < cm.lineCount() - 1 ? cm.getLine(cursor.line + 1) : "";
|
|
88
|
+
var nextIndent = nextLine ? nextLine.match(/^\s*/)[0] : "";
|
|
89
|
+
|
|
90
|
+
if (nextLine === "" || nextIndent.length < currentIndent.length) {
|
|
91
|
+
let reducedIndent = currentIndent.slice(0, Math.max(0, currentIndent.length - cm.getOption("indentUnit")));
|
|
92
|
+
cm.replaceSelection("\n" + reducedIndent);
|
|
93
|
+
} else {
|
|
94
|
+
cm.replaceSelection("\n" + currentIndent);
|
|
95
|
+
}
|
|
96
|
+
} else if (/:\s*$/.test(line)) {
|
|
97
|
+
cm.replaceSelection("\n" + currentIndent + Array(cm.getOption("indentUnit") + 1).join(" "));
|
|
98
|
+
} else {
|
|
99
|
+
cm.replaceSelection("\n" + currentIndent);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"Shift-Enter": function(cm) {
|
|
103
|
+
// Always create a new line with the same indentation as the current line
|
|
104
|
+
var cursor = cm.getCursor();
|
|
105
|
+
var line = cm.getLine(cursor.line);
|
|
106
|
+
var currentIndent = line.match(/^\s*/)[0];
|
|
107
|
+
cm.replaceSelection("\n" + currentIndent);
|
|
108
|
+
},
|
|
109
|
+
"Ctrl-.": "toggleComment", // For Windows/Linux
|
|
110
|
+
"Cmd-.": "toggleComment", // For Mac
|
|
111
|
+
"Ctrl-Space": "autocomplete",
|
|
112
|
+
// Shift-Tab navigates to previous placeholder when available
|
|
113
|
+
"Shift-Tab": (cm) => {
|
|
114
|
+
const st = cm.state && cm.state.snippetPH;
|
|
115
|
+
if (st && Array.isArray(st.markers) && st.markers.length) {
|
|
116
|
+
let i = st.index - 1;
|
|
117
|
+
while (i >= 0) {
|
|
118
|
+
const pos = st.markers[i] && st.markers[i].find();
|
|
119
|
+
if (pos) {
|
|
120
|
+
st.index = i;
|
|
121
|
+
cm.setSelection(pos.from, pos.to);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
i--;
|
|
125
|
+
}
|
|
126
|
+
// No previous valid marker; keep current selection as-is
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
// Placeholder navigation for snippets
|
|
130
|
+
"Ctrl-]": cm => this.selectNextPlaceholder(cm),
|
|
131
|
+
"Cmd-]": cm => this.selectNextPlaceholder(cm),
|
|
132
|
+
"Ctrl-[": cm => this.selectPrevPlaceholder(cm),
|
|
133
|
+
"Cmd-[": cm => this.selectPrevPlaceholder(cm),
|
|
134
|
+
|
|
135
|
+
"Backspace": function(cm) {
|
|
136
|
+
// Get cursor position
|
|
137
|
+
const cursor = cm.getCursor();
|
|
138
|
+
const line = cm.getLine(cursor.line);
|
|
139
|
+
|
|
140
|
+
// Check if we're at an empty line with indentation
|
|
141
|
+
if (line.trim() === '' && cursor.ch > 0 && cursor.ch % 4 === 0) {
|
|
142
|
+
// Delete 4 spaces (one indentation level)
|
|
143
|
+
cm.replaceRange("",
|
|
144
|
+
{line: cursor.line, ch: cursor.ch - 4},
|
|
145
|
+
{line: cursor.line, ch: cursor.ch});
|
|
146
|
+
} else {
|
|
147
|
+
// Normal backspace behavior
|
|
148
|
+
CodeMirror.commands.delCharBefore(cm);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Show snippet hint popup as user types letters/underscore
|
|
157
|
+
editor.on('inputRead', function(cm, change) {
|
|
158
|
+
try {
|
|
159
|
+
if (!change || !change.text) return;
|
|
160
|
+
// Trigger on single-character word-like input
|
|
161
|
+
if (change.text.length === 1 && /^[A-Za-z_]$/.test(change.text[0])) {
|
|
162
|
+
self.triggerSnippetHint(cm);
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// fail-safe
|
|
166
|
+
console.debug('snippet hint inputRead error', e);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return editor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async loadAddons() {
|
|
175
|
+
// Only load addons if CodeMirror exists
|
|
176
|
+
if (typeof CodeMirror === 'undefined') {
|
|
177
|
+
console.error("CodeMirror not found! Addons will not be loaded.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const addons = [
|
|
182
|
+
// Core editing helpers
|
|
183
|
+
'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/matchbrackets.min.js',
|
|
184
|
+
'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/closebrackets.min.js',
|
|
185
|
+
'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/comment/comment.min.js',
|
|
186
|
+
// Show-hint for autocomplete popup
|
|
187
|
+
'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.js',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const styles = [
|
|
191
|
+
'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/hint/show-hint.min.css',
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const loadScript = (src) => {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const script = document.createElement('script');
|
|
197
|
+
script.src = src;
|
|
198
|
+
script.onload = resolve;
|
|
199
|
+
script.onerror = reject;
|
|
200
|
+
document.head.appendChild(script);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const loadStyle = (href) => {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
// Avoid duplicate loads
|
|
207
|
+
if ([...document.styleSheets].some(s => s.href === href)) return resolve();
|
|
208
|
+
const link = document.createElement('link');
|
|
209
|
+
link.rel = 'stylesheet';
|
|
210
|
+
link.href = href;
|
|
211
|
+
link.onload = resolve;
|
|
212
|
+
link.onerror = reject;
|
|
213
|
+
document.head.appendChild(link);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const href of styles) {
|
|
218
|
+
try {
|
|
219
|
+
await loadStyle(href);
|
|
220
|
+
console.log(`Loaded CSS: ${href}`);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(`Failed to load CSS: ${href}`, error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const addon of addons) {
|
|
227
|
+
try {
|
|
228
|
+
await loadScript(addon);
|
|
229
|
+
console.log(`Loaded: ${addon}`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error(`Failed to load: ${addon}`, error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Replaces the tab key press with spaces for consistent indentation.
|
|
238
|
+
* @param {Object} cm - The CodeMirror instance.
|
|
239
|
+
*/
|
|
240
|
+
replaceTabWithSpaces(cm) {
|
|
241
|
+
let spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
|
|
242
|
+
cm.replaceSelection(spaces);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------- Snippets ----------
|
|
246
|
+
// Simple snippet dictionary (keyword -> template and initial selection word)
|
|
247
|
+
getSnippets() {
|
|
248
|
+
return {
|
|
249
|
+
// Norwegian-friendly placeholders
|
|
250
|
+
"for": {
|
|
251
|
+
template: "for n in range(start, stopp, avstand):\n print(n)",
|
|
252
|
+
caretWords: ["start", "stopp", "avstand"]
|
|
253
|
+
},
|
|
254
|
+
"if": {
|
|
255
|
+
template: "if betingelse:\n pass",
|
|
256
|
+
caretWords: ["betingelse", "pass"]
|
|
257
|
+
},
|
|
258
|
+
"while": {
|
|
259
|
+
template: "while betingelse:\n pass",
|
|
260
|
+
caretWords: ["betingelse", "pass"]
|
|
261
|
+
},
|
|
262
|
+
"if-else": {
|
|
263
|
+
template: "if betingelse:\n pass\nelse:\n pass",
|
|
264
|
+
caretWords: ["betingelse"]
|
|
265
|
+
},
|
|
266
|
+
"funksjon": {
|
|
267
|
+
template: "def funksjonsnavn(variabel):\n return funksjonsuttrykk",
|
|
268
|
+
caretWords: ["funksjonsnavn", "variabel", "funksjonsuttrykk"]
|
|
269
|
+
},
|
|
270
|
+
"sum": {
|
|
271
|
+
template: "s = 0\nfor n in range(start, stopp, avstand):\n a = formel\n s = s + a",
|
|
272
|
+
caretWords: ["start", "stopp", "avstand", "formel"]
|
|
273
|
+
},
|
|
274
|
+
"print": {
|
|
275
|
+
template: "print(verdi)",
|
|
276
|
+
caretWords: ["verdi"]
|
|
277
|
+
},
|
|
278
|
+
"print-f": {
|
|
279
|
+
template: "print(f\"{variabel = }\")",
|
|
280
|
+
caretWords: ["variabel"]
|
|
281
|
+
},
|
|
282
|
+
"likning": {
|
|
283
|
+
template: "for x in range(start, stopp, avstand):\n if venstre_side == høyre_side:\n print(x)",
|
|
284
|
+
caretWords: ["start", "stopp", "avstand", "venstre_side", "høyre_side"]
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Expand snippet if the current line is exactly a keyword (ignoring leading spaces)
|
|
290
|
+
tryExpandSnippet(cm) {
|
|
291
|
+
const cur = cm.getCursor();
|
|
292
|
+
const lineText = cm.getLine(cur.line);
|
|
293
|
+
const leadingWSMatch = lineText.match(/^\s*/);
|
|
294
|
+
const leadingWS = leadingWSMatch ? leadingWSMatch[0] : "";
|
|
295
|
+
const trimmed = lineText.slice(leadingWS.length);
|
|
296
|
+
|
|
297
|
+
const snippets = this.getSnippets();
|
|
298
|
+
const snip = snippets[trimmed];
|
|
299
|
+
if (!snip) return false;
|
|
300
|
+
|
|
301
|
+
const { template, caretWords } = snip;
|
|
302
|
+
|
|
303
|
+
// Indent template according to current indentation and editor indent unit
|
|
304
|
+
const indented = this.buildIndentedTemplate(cm, leadingWS, template);
|
|
305
|
+
|
|
306
|
+
cm.operation(() => {
|
|
307
|
+
// Replace entire current line with the snippet to avoid duplicate indentation
|
|
308
|
+
cm.replaceRange(
|
|
309
|
+
indented,
|
|
310
|
+
{ line: cur.line, ch: 0 },
|
|
311
|
+
{ line: cur.line, ch: lineText.length }
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Prepare placeholders and select the first, if any
|
|
315
|
+
this.postInsertSnippet(cm, cur.line, indented, caretWords);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Build an indented template that honors the editor's indentUnit.
|
|
322
|
+
// For each template line, count its leading whitespace (spaces/tabs), convert to indent levels,
|
|
323
|
+
// and prefix with the current line's indentation plus that many indent levels.
|
|
324
|
+
buildIndentedTemplate(cm, leadingWS, template) {
|
|
325
|
+
const unit = cm.getOption('indentUnit') || 4;
|
|
326
|
+
const indentStr = ' '.repeat(unit);
|
|
327
|
+
const toLevels = (ws) => {
|
|
328
|
+
let count = 0;
|
|
329
|
+
for (let i = 0; i < ws.length; i++) {
|
|
330
|
+
count += (ws[i] === '\t') ? unit : 1;
|
|
331
|
+
}
|
|
332
|
+
return Math.floor(count / unit);
|
|
333
|
+
};
|
|
334
|
+
const lines = template.split('\n');
|
|
335
|
+
return lines.map(line => {
|
|
336
|
+
const m = line.match(/^[ \t]*/)[0];
|
|
337
|
+
const level = toLevels(m);
|
|
338
|
+
const content = line.slice(m.length);
|
|
339
|
+
return leadingWS + indentStr.repeat(level) + content;
|
|
340
|
+
}).join('\n');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Trigger snippet hint popup as the user types
|
|
344
|
+
triggerSnippetHint(cm) {
|
|
345
|
+
if (!CodeMirror || !CodeMirror.showHint) return;
|
|
346
|
+
CodeMirror.showHint(cm, (cm_) => this.snippetHint(cm_), { completeSingle: false });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Custom hint provider that suggests snippet keywords and inserts templates
|
|
350
|
+
snippetHint(cm) {
|
|
351
|
+
const cur = cm.getCursor();
|
|
352
|
+
const lineText = cm.getLine(cur.line);
|
|
353
|
+
const leadingWSMatch = lineText.match(/^\s*/);
|
|
354
|
+
const leadingWS = leadingWSMatch ? leadingWSMatch[0] : "";
|
|
355
|
+
|
|
356
|
+
// Determine the current word before the cursor
|
|
357
|
+
let startCh = cur.ch;
|
|
358
|
+
while (startCh > 0 && /[A-Za-z_]/.test(lineText.charAt(startCh - 1))) startCh--;
|
|
359
|
+
const prefix = lineText.slice(startCh, cur.ch);
|
|
360
|
+
|
|
361
|
+
// Only offer snippet completions when typing at indentation start
|
|
362
|
+
if (startCh !== leadingWS.length) {
|
|
363
|
+
return { list: [], from: { line: cur.line, ch: cur.ch }, to: { line: cur.line, ch: cur.ch } };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const snippets = this.getSnippets();
|
|
367
|
+
const keys = Object.keys(snippets).filter(k => k.indexOf(prefix) === 0);
|
|
368
|
+
|
|
369
|
+
const list = keys.map(key => {
|
|
370
|
+
const { template } = snippets[key];
|
|
371
|
+
return {
|
|
372
|
+
text: key,
|
|
373
|
+
displayText: `${key} — ${template.split('\n')[0]}…`,
|
|
374
|
+
hint: (cmPick) => {
|
|
375
|
+
// Replace the current word with the snippet template, with indentation
|
|
376
|
+
const { template: tpl, caretWords } = snippets[key];
|
|
377
|
+
const indented = this.buildIndentedTemplate(cmPick, leadingWS, tpl);
|
|
378
|
+
cmPick.operation(() => {
|
|
379
|
+
// Replace from start of line to cursor to include indentation
|
|
380
|
+
cmPick.replaceRange(
|
|
381
|
+
indented,
|
|
382
|
+
{ line: cur.line, ch: 0 },
|
|
383
|
+
{ line: cur.line, ch: cur.ch }
|
|
384
|
+
);
|
|
385
|
+
// Prepare placeholders and select the first
|
|
386
|
+
this.postInsertSnippet(cmPick, cur.line, indented, caretWords);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
list,
|
|
394
|
+
from: { line: cur.line, ch: startCh },
|
|
395
|
+
to: { line: cur.line, ch: cur.ch }
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// After inserting a snippet, create live markers for placeholders and select the first
|
|
400
|
+
postInsertSnippet(cm, baseLine, indented, caretWords) {
|
|
401
|
+
const words = Array.isArray(caretWords) ? caretWords : (caretWords ? [caretWords] : []);
|
|
402
|
+
const lines = indented.split('\n');
|
|
403
|
+
const markers = [];
|
|
404
|
+
|
|
405
|
+
if (words.length) {
|
|
406
|
+
for (let i = 0; i < lines.length; i++) {
|
|
407
|
+
for (const w of words) {
|
|
408
|
+
let idx = -1, fromCh = 0;
|
|
409
|
+
while ((idx = lines[i].indexOf(w, fromCh)) !== -1) {
|
|
410
|
+
const from = { line: baseLine + i, ch: idx };
|
|
411
|
+
const to = { line: baseLine + i, ch: idx + w.length };
|
|
412
|
+
const m = cm.getDoc().markText(from, to, { inclusiveLeft: true, inclusiveRight: true });
|
|
413
|
+
markers.push(m);
|
|
414
|
+
fromCh = idx + w.length;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
cm.state.snippetPH = { markers, index: 0 };
|
|
421
|
+
|
|
422
|
+
// Helper to select current marker if it still exists
|
|
423
|
+
const selectIndex = (idx) => {
|
|
424
|
+
if (!markers.length) return false;
|
|
425
|
+
const mk = markers[idx];
|
|
426
|
+
if (!mk) return false;
|
|
427
|
+
const pos = mk.find();
|
|
428
|
+
if (!pos) return false; // marker cleared by edit
|
|
429
|
+
cm.setSelection(pos.from, pos.to);
|
|
430
|
+
return true;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
if (!selectIndex(0)) {
|
|
434
|
+
// Fallback: put cursor at end of first inserted line
|
|
435
|
+
cm.setCursor({ line: baseLine, ch: lines[0].length });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Move to the next existing placeholder marker
|
|
440
|
+
selectNextPlaceholder(cm) {
|
|
441
|
+
const st = cm.state && cm.state.snippetPH;
|
|
442
|
+
if (!st || !st.markers || !st.markers.length) return;
|
|
443
|
+
let i = st.index + 1;
|
|
444
|
+
while (i < st.markers.length) {
|
|
445
|
+
const pos = st.markers[i] && st.markers[i].find();
|
|
446
|
+
if (pos) {
|
|
447
|
+
st.index = i;
|
|
448
|
+
cm.setSelection(pos.from, pos.to);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
i++;
|
|
452
|
+
}
|
|
453
|
+
// No further placeholders: clear state
|
|
454
|
+
this.clearSnippetPlaceholders(cm);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Move to the previous existing placeholder marker
|
|
458
|
+
selectPrevPlaceholder(cm) {
|
|
459
|
+
const st = cm.state && cm.state.snippetPH;
|
|
460
|
+
if (!st || !st.markers || !st.markers.length) return;
|
|
461
|
+
let i = st.index - 1;
|
|
462
|
+
while (i >= 0) {
|
|
463
|
+
const pos = st.markers[i] && st.markers[i].find();
|
|
464
|
+
if (pos) {
|
|
465
|
+
st.index = i;
|
|
466
|
+
cm.setSelection(pos.from, pos.to);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
i--;
|
|
470
|
+
}
|
|
471
|
+
// No previous: keep current or clear if current is gone
|
|
472
|
+
const cur = st.markers[st.index] && st.markers[st.index].find();
|
|
473
|
+
if (!cur) this.clearSnippetPlaceholders(cm);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
clearSnippetPlaceholders(cm) {
|
|
477
|
+
const st = cm.state && cm.state.snippetPH;
|
|
478
|
+
if (!st) return;
|
|
479
|
+
if (st.markers) {
|
|
480
|
+
st.markers.forEach(m => { try { m.clear(); } catch(_){} });
|
|
481
|
+
}
|
|
482
|
+
delete cm.state.snippetPH;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Dynamically determines and applies the current theme based on user preference.
|
|
487
|
+
* @returns {string} - The theme name to be applied.
|
|
488
|
+
*/
|
|
489
|
+
getCurrentTheme() {
|
|
490
|
+
const mode = document.documentElement.getAttribute('data-mode');
|
|
491
|
+
const lightTheme = "github-light";
|
|
492
|
+
const darkTheme = "github-dark-high-contrast";
|
|
493
|
+
|
|
494
|
+
if (mode === 'dark') {
|
|
495
|
+
return darkTheme;
|
|
496
|
+
} else if (mode === 'light') {
|
|
497
|
+
return lightTheme;
|
|
498
|
+
} else if (mode === 'auto') {
|
|
499
|
+
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
500
|
+
return prefersDarkScheme ? darkTheme : lightTheme;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Sets up a listener to dynamically update the theme when the user changes it.
|
|
506
|
+
*/
|
|
507
|
+
setupThemeListener() {
|
|
508
|
+
const observer = new MutationObserver(mutations => {
|
|
509
|
+
mutations.forEach(mutation => {
|
|
510
|
+
if (mutation.attributeName === 'data-mode') {
|
|
511
|
+
this.editor.setOption('theme', this.getCurrentTheme());
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
observer.observe(document.documentElement, {
|
|
517
|
+
attributes: true,
|
|
518
|
+
attributeFilter: ['data-mode'],
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Set the initial theme when the editor is first initialized
|
|
522
|
+
this.editor.setOption('theme', this.getCurrentTheme());
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Adds a custom overlay for highlighting specific comments for
|
|
527
|
+
* Supports # TODO, # FIKS MEG, # FYLL INN, # NOTE, # FIKSMEG, # IGNORER
|
|
528
|
+
* @returns {Object} - The overlay mode configuration for CodeMirror.
|
|
529
|
+
*/
|
|
530
|
+
addCommentOverlay(editor) {
|
|
531
|
+
editor.addOverlay({
|
|
532
|
+
token: function(stream) {
|
|
533
|
+
const keywords = [
|
|
534
|
+
"# TODO",
|
|
535
|
+
"# FIKSMEG",
|
|
536
|
+
"# FIKS MEG",
|
|
537
|
+
"# NOTE",
|
|
538
|
+
"# FYLL INN",
|
|
539
|
+
"# IGNORER",
|
|
540
|
+
"# IKKE RØR",
|
|
541
|
+
"# FOKUS",
|
|
542
|
+
"# FORKLARING",
|
|
543
|
+
"# <--",
|
|
544
|
+
"# MERK",
|
|
545
|
+
"????",
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
for (const keyword of keywords) {
|
|
549
|
+
if (stream.match(keyword)) {
|
|
550
|
+
// Special-case: map "# <--" to the same class as TODO
|
|
551
|
+
if (keyword === "# <--") {
|
|
552
|
+
return "todo";
|
|
553
|
+
}
|
|
554
|
+
return keyword
|
|
555
|
+
.replace("# ", "")
|
|
556
|
+
.toLowerCase()
|
|
557
|
+
.replace(" ", "")
|
|
558
|
+
.replace(/\?+/g, "question");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
while (stream.next() != null && !keywords.some(keyword => stream.match(keyword, false))) {}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
return editor;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Sets the code in the editor to a specified value.
|
|
571
|
+
* @param {string} code - The code to set in the editor.
|
|
572
|
+
*/
|
|
573
|
+
setValue(code) {
|
|
574
|
+
this.editor.setValue(code);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Gets the current value (code) from the editor.
|
|
579
|
+
* @returns {string} - The code currently in the editor.
|
|
580
|
+
*/
|
|
581
|
+
getValue() {
|
|
582
|
+
return this.editor.getValue();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Clears the editor content and optionally resets it to its initial value.
|
|
587
|
+
* @param {string} [initialValue=""] - The initial code value to reset the editor to (optional).
|
|
588
|
+
*/
|
|
589
|
+
resetEditor(initialValue = "") {
|
|
590
|
+
this.editor.setValue(initialValue);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Highlights a specific line in the editor (useful for debugging or showing errors).
|
|
595
|
+
* @param {number} line - The line number to highlight (0-indexed).
|
|
596
|
+
*/
|
|
597
|
+
highlightLine(line) {
|
|
598
|
+
console.log("Highlighting line", line);
|
|
599
|
+
this.editor.addLineClass(line, "background", "cm-highlight");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
removehighlightLine(line) {
|
|
603
|
+
this.editor.removeLineClass(line, "background", "line-highlight-red");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
clearLineHighlights() {
|
|
607
|
+
for (let i = 0; i < this.editor.lineCount(); i++) {
|
|
608
|
+
this.editor.removeLineClass(i, "background", "cm-highlight");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Scrolls the editor to a specified line, useful for showing errors or results.
|
|
614
|
+
* @param {number} line - The line number to scroll to (0-indexed).
|
|
615
|
+
*/
|
|
616
|
+
scrollToLine(line) {
|
|
617
|
+
this.editor.scrollIntoView({ line: line, ch: 0 }, 200);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
refreshOnVisibilityChange() {
|
|
621
|
+
const editorElement = this.editor.getWrapperElement();
|
|
622
|
+
const observer = new IntersectionObserver((entries) => {
|
|
623
|
+
entries.forEach(entry => {
|
|
624
|
+
if (entry.isIntersecting) {
|
|
625
|
+
this.editor.refresh();
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}, { threshold: 0.1 });
|
|
629
|
+
|
|
630
|
+
observer.observe(editorElement);
|
|
631
|
+
}
|
|
632
|
+
}
|