solarite 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/build/build.bat +3 -0
- package/build/build.js +139 -0
- package/build/lib/rollup.min.js +11 -0
- package/build/lib/source-map.min.js +1 -0
- package/build/lib/terser.min.js +1 -0
- package/dist/Solarite-debug.js +4143 -0
- package/dist/Solarite.js +3740 -0
- package/dist/Solarite.min.js +4 -0
- package/docs/index.md +423 -0
- package/docs/js/Playground.js +184 -0
- package/docs/js/codemirror/codemirror6.js +32036 -0
- package/docs/js/codemirror/themeSolarIce.js +312 -0
- package/docs/js/documentation.js +32 -0
- package/docs/js/ui/CodeEditor.js +840 -0
- package/docs/js/ui/DarkToggle.js +52 -0
- package/docs/js/ui/FlexResizer.js +142 -0
- package/docs/js/util/Draggable2.js +151 -0
- package/docs/js/util/Errors.js +9 -0
- package/docs/js/util/Html.js +147 -0
- package/docs/js/util/Icons.js +623 -0
- package/docs/js/util/Input.js +253 -0
- package/docs/js/util/Util.js +88 -0
- package/docs/js/util/delve.js +43 -0
- package/docs/media/FiraCode400.woff2 +0 -0
- package/docs/media/cabin-latin-700.woff2 +0 -0
- package/docs/media/documentation.css +93 -0
- package/docs/media/eternium.css +1123 -0
- package/docs/media/solarite-machine.webp +0 -0
- package/index.html +325 -0
- package/package.json +33 -0
- package/readme.md +3 -0
- package/src/solarite/ExprPath.js +554 -0
- package/src/solarite/MultiValueMap.js +65 -0
- package/src/solarite/NodeGroup.js +706 -0
- package/src/solarite/NodeGroupManager.js +582 -0
- package/src/solarite/Shell.js +307 -0
- package/src/solarite/Solarite.js +19 -0
- package/src/solarite/Template.js +85 -0
- package/src/solarite/Util.js +264 -0
- package/src/solarite/createSolarite.js +267 -0
- package/src/solarite/getArg.js +99 -0
- package/src/solarite/hash.js +101 -0
- package/src/solarite/r.js +143 -0
- package/src/solarite/udomdiff.js +233 -0
- package/src/solarite/watch.js +302 -0
- package/src/solarite/watch2.js +439 -0
- package/src/unused/FastLookupArray.js +54 -0
- package/src/unused/Hashes.js +339 -0
- package/src/unused/InUse.test.js +92 -0
- package/src/unused/InUseMap.js +98 -0
- package/src/unused/LinkedList.js +117 -0
- package/src/unused/LinkedList.test.js +115 -0
- package/src/unused/Perf.js +47 -0
- package/src/unused/Template.js +108 -0
- package/src/util/Errors.js +9 -0
- package/src/util/Util.js +88 -0
- package/src/util/delve.js +43 -0
- package/tests/Benchmark.test.js +319 -0
- package/tests/NodeGroup.test.js +115 -0
- package/tests/Shell.test.js +75 -0
- package/tests/Solarite.test.js +2896 -0
- package/tests/Testimony.js +602 -0
- package/tests/index.html +75 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autocompletion,
|
|
3
|
+
bracketMatching,
|
|
4
|
+
closeBrackets,
|
|
5
|
+
closeBracketsKeymap,
|
|
6
|
+
closeSearchPanel,
|
|
7
|
+
Compartment,
|
|
8
|
+
completionKeymap,
|
|
9
|
+
crosshairCursor,
|
|
10
|
+
css,
|
|
11
|
+
cssParser,
|
|
12
|
+
defaultHighlightStyle,
|
|
13
|
+
defaultKeymap,
|
|
14
|
+
drawSelection,
|
|
15
|
+
EditorState,
|
|
16
|
+
EditorView,
|
|
17
|
+
foldKeymap,
|
|
18
|
+
highlightActiveLine,
|
|
19
|
+
highlightActiveLineGutter,
|
|
20
|
+
highlightSelectionMatches,
|
|
21
|
+
highlightSpecialChars,
|
|
22
|
+
history,
|
|
23
|
+
historyKeymap,
|
|
24
|
+
html,
|
|
25
|
+
htmlParser,
|
|
26
|
+
indentOnInput,
|
|
27
|
+
indentUnit,
|
|
28
|
+
indentWithTab,
|
|
29
|
+
javascript,
|
|
30
|
+
json,
|
|
31
|
+
jsParser,
|
|
32
|
+
keymap,
|
|
33
|
+
lineNumbers,
|
|
34
|
+
LRLanguage,
|
|
35
|
+
markdown,
|
|
36
|
+
openSearchPanel,
|
|
37
|
+
parseMixed,
|
|
38
|
+
php,
|
|
39
|
+
phpParser,
|
|
40
|
+
python,
|
|
41
|
+
rectangularSelection,
|
|
42
|
+
redo,
|
|
43
|
+
redoDepth,
|
|
44
|
+
searchKeymap,
|
|
45
|
+
searchPanelOpen,
|
|
46
|
+
sql,
|
|
47
|
+
StateEffect,
|
|
48
|
+
StateField,
|
|
49
|
+
syntaxHighlighting,
|
|
50
|
+
undo,
|
|
51
|
+
undoDepth
|
|
52
|
+
} from '../codemirror/codemirror6.js';
|
|
53
|
+
import themeSolarIce from "../codemirror/themeSolarIce.js";
|
|
54
|
+
import {r, Solarite, getArg, ArgType} from "../../../src/solarite/Solarite.js";
|
|
55
|
+
import Icons from "../util/Icons.js";
|
|
56
|
+
import Util from "../util/Util.js";
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wraps CodeMirror into a web component and provides some useful functions and default settings.
|
|
62
|
+
* TODO: Emmet support for html: https://discuss.codemirror.net/t/is-it-able-to-use-tab-to-generate-code-snippets-for-html-tag/5502
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* new CodeEditor('sql', 'SELECT * FROM users', {}, 'undo redo | run'); */
|
|
66
|
+
export default class CodeEditor extends Solarite {
|
|
67
|
+
|
|
68
|
+
/** @type {EditorView} */
|
|
69
|
+
view;
|
|
70
|
+
|
|
71
|
+
extensions = [];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @type {Callbacks|function}
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* codeEditor.onChange.push(update => {
|
|
78
|
+
* console.log(update.state.doc.toString();
|
|
79
|
+
* });
|
|
80
|
+
* */
|
|
81
|
+
onChange = Util.callback();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Called every time CodeMirror selection or content changes.
|
|
85
|
+
* @type {Callbacks|function} */
|
|
86
|
+
onInternalChange = Util.callback();
|
|
87
|
+
|
|
88
|
+
/** @type {Callbacks|function(range:{start: {line:int, column:int}, end: {line:int, column:int}})} */
|
|
89
|
+
onSelectionChange = Util.callback();
|
|
90
|
+
|
|
91
|
+
/** @type {Callbacks|function} */
|
|
92
|
+
onOptionChange = Util.callback();
|
|
93
|
+
|
|
94
|
+
allowChanges = true;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Serialized selection range. Line numbers start at 0.
|
|
98
|
+
* @type {string} */
|
|
99
|
+
lastSelection = '0-0';
|
|
100
|
+
|
|
101
|
+
/** @type {HTMLElement} */
|
|
102
|
+
editor;
|
|
103
|
+
|
|
104
|
+
/** @type {?CodeEditorToolbar} */
|
|
105
|
+
toolbar;
|
|
106
|
+
|
|
107
|
+
/** @type {string} */
|
|
108
|
+
language;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compartments wrap extensions and allow changing thier settings after init. */
|
|
112
|
+
compartments = {
|
|
113
|
+
lineWrapping: new Compartment(),
|
|
114
|
+
language: new Compartment(),
|
|
115
|
+
tabSize: new Compartment(),
|
|
116
|
+
theme: new Compartment()
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
wordWrap = false;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Example languageConfig for SQL:
|
|
123
|
+
* https://github.com/codemirror/lang-sql#----interface----sqlconfig
|
|
124
|
+
* {
|
|
125
|
+
* upperCaseKeywords: true,
|
|
126
|
+
* schema: { // Define tables and their columns for auto-complete.
|
|
127
|
+
* users: ['email', 'passwordHash'],
|
|
128
|
+
* stats: ['id', 'created']
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* @param value {string}
|
|
133
|
+
* @param language {?string} Can be the name of the language or one of the file extensions.
|
|
134
|
+
* Supported languages: php, php-plain, javascript, html, markdown, sql, css
|
|
135
|
+
* @param languageconfig {?Object} Options for CodeMirror.
|
|
136
|
+
* @param toolbar {string|Object<html:Template, update:function>[]} Names of buttons from CodeEditiorToolbar.buttonTemplates or an array of HTMLElements to use as buttons.
|
|
137
|
+
* @param options {object}
|
|
138
|
+
* @param options.wordWrap {boolean}
|
|
139
|
+
* @param options.tabSize {int}
|
|
140
|
+
* @param options.fontSize {int} */
|
|
141
|
+
constructor({value='', language=null, languageconfig=null, toolbar='', options={}}={}) {
|
|
142
|
+
super();
|
|
143
|
+
|
|
144
|
+
value = getArg(this, 'value', value, ArgType.String);
|
|
145
|
+
this.language = (language||'php').toLowerCase();
|
|
146
|
+
this.languageConfig = languageconfig || {};
|
|
147
|
+
this.wordWrap = options.wordWrap || false;
|
|
148
|
+
this.tabSize = options.tabSize || 4;
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
// https://github.com/codemirror/lang-php
|
|
153
|
+
if (this.language === 'php-plain')
|
|
154
|
+
this.languageConfig.plain = true;
|
|
155
|
+
|
|
156
|
+
// TODO: Use this to specify markdown code block languages:
|
|
157
|
+
// And add keybindings for ctrl+1 headers, etc.
|
|
158
|
+
// https://github.com/codemirror/lang-markdown
|
|
159
|
+
|
|
160
|
+
this.render();
|
|
161
|
+
|
|
162
|
+
let state = EditorState.create({
|
|
163
|
+
doc: value,
|
|
164
|
+
extensions: this.getExtensions(options.wordWrap)
|
|
165
|
+
})
|
|
166
|
+
this.view = new EditorView({
|
|
167
|
+
state,
|
|
168
|
+
parent: this.editor
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Create the toolbar
|
|
172
|
+
if (toolbar) {
|
|
173
|
+
this.toolbar = new CodeEditorToolbar({ed: this, buttons: toolbar});
|
|
174
|
+
this.insertBefore(this.toolbar, this.editor);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// Watch for dark attribute change on <html> element.
|
|
179
|
+
// So we can update our own theme in response.
|
|
180
|
+
this.onConnect.push(() => {
|
|
181
|
+
if (this.ownerDocument.documentElement) {
|
|
182
|
+
const observer = new MutationObserver((mutations) => {
|
|
183
|
+
mutations.forEach((mutation) => {
|
|
184
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'dark') {
|
|
185
|
+
let state = mutation.target.hasAttribute('dark');
|
|
186
|
+
this.setTheme(themeSolarIce(this.language, state));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
observer.observe(this.ownerDocument.documentElement, {attributes: true});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.onInternalChange.push((update, codeEditor) => {
|
|
196
|
+
let sel = update.state.selection.main;
|
|
197
|
+
let lineObj = update.state.doc.lineAt(sel.head);
|
|
198
|
+
|
|
199
|
+
let newSelection = lineObj.number + '-' + (sel.head - lineObj.from);
|
|
200
|
+
|
|
201
|
+
if (newSelection !== codeEditor.lastSelection) {
|
|
202
|
+
if (codeEditor.allowChanges && codeEditor.onSelectionChange.length) {
|
|
203
|
+
let range = {
|
|
204
|
+
start: {
|
|
205
|
+
line: lineObj.number - 1,
|
|
206
|
+
column: sel.head - lineObj.from
|
|
207
|
+
},
|
|
208
|
+
end: {}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
lineObj = sel.head === sel.anchor ? lineObj : update.state.doc.lineAt(sel.anchor);
|
|
212
|
+
range.end.line = lineObj.number - 1;
|
|
213
|
+
range.end.column = sel.anchor - lineObj.from
|
|
214
|
+
|
|
215
|
+
codeEditor.onSelectionChange(range, update);
|
|
216
|
+
}
|
|
217
|
+
this.lastSelection = newSelection;
|
|
218
|
+
if (this.toolbar)
|
|
219
|
+
this.toolbar.update(update);
|
|
220
|
+
}
|
|
221
|
+
// console.log(startLine, startColumn, endLine, endColumn, update)
|
|
222
|
+
if (this.allowChanges && update.changedRanges.length && this.onChange.length) {
|
|
223
|
+
let codeChange = {
|
|
224
|
+
beforeStart: update.changedRanges[0].fromA,
|
|
225
|
+
beforeEnd: update.changedRanges[0].toA,
|
|
226
|
+
afterStart: update.changedRanges[0].fromB,
|
|
227
|
+
afterEnd: update.changedRanges[0].toB,
|
|
228
|
+
text: update.state.doc.sliceString(update.changedRanges[0].fromB, update.changedRanges[0].toB)
|
|
229
|
+
};
|
|
230
|
+
this.onChange(codeChange, update);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
Object.defineProperty(this, 'value', {
|
|
236
|
+
get() {
|
|
237
|
+
return this.getValue();
|
|
238
|
+
},
|
|
239
|
+
set(val) {
|
|
240
|
+
this.setValue(val, false);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getExtensions(wordWrap=false, isDark=undefined) {
|
|
246
|
+
let doc = this.ownerDocument || window.top.document;
|
|
247
|
+
if (isDark === undefined)
|
|
248
|
+
isDark = doc.documentElement.hasAttribute('dark');
|
|
249
|
+
|
|
250
|
+
let languageExtensions = this.getLanguageExtension(this.language, this.languageConfig)
|
|
251
|
+
return [
|
|
252
|
+
//basicSetup,
|
|
253
|
+
|
|
254
|
+
lineNumbers(),
|
|
255
|
+
highlightActiveLineGutter(),
|
|
256
|
+
highlightSpecialChars(),
|
|
257
|
+
history(),
|
|
258
|
+
//foldGutter(),
|
|
259
|
+
drawSelection(),
|
|
260
|
+
indentUnit.of("\t"),
|
|
261
|
+
this.compartments.tabSize.of(EditorState.tabSize.of(this.tabSize)),
|
|
262
|
+
this.compartments.lineWrapping.of(wordWrap ? EditorView.lineWrapping : []),
|
|
263
|
+
EditorState.allowMultipleSelections.of(true),
|
|
264
|
+
indentOnInput(),
|
|
265
|
+
bracketMatching(),
|
|
266
|
+
closeBrackets(),
|
|
267
|
+
autocompletion(),
|
|
268
|
+
rectangularSelection(),
|
|
269
|
+
crosshairCursor(),
|
|
270
|
+
highlightActiveLine(),
|
|
271
|
+
highlightSelectionMatches(),
|
|
272
|
+
keymap.of([
|
|
273
|
+
indentWithTab,
|
|
274
|
+
...closeBracketsKeymap,
|
|
275
|
+
{
|
|
276
|
+
key: 'Tab', // TODO: What does this do?
|
|
277
|
+
//run: target => acceptCompletion(target),
|
|
278
|
+
},
|
|
279
|
+
...defaultKeymap,
|
|
280
|
+
...historyKeymap,
|
|
281
|
+
...foldKeymap,
|
|
282
|
+
...completionKeymap,
|
|
283
|
+
...searchKeymap
|
|
284
|
+
]),
|
|
285
|
+
|
|
286
|
+
Extensions.changeDetection(this),
|
|
287
|
+
Extensions.asyncField,
|
|
288
|
+
|
|
289
|
+
this.compartments.theme.of(themeSolarIce(this.language, isDark)),
|
|
290
|
+
|
|
291
|
+
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
|
|
292
|
+
|
|
293
|
+
this.compartments.language.of(languageExtensions)
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
focus() {
|
|
298
|
+
this.view.focus();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set the colors, fonts, and sizes.
|
|
303
|
+
* @param theme
|
|
304
|
+
* @returns {Promise<void>} */
|
|
305
|
+
async setTheme(theme) {
|
|
306
|
+
this.view.dispatch({
|
|
307
|
+
effects: this.compartments.theme.reconfigure(theme)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getValue() {
|
|
312
|
+
return this.view.viewState.state.doc.toString()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
undo() {
|
|
316
|
+
undo(this.view);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
redo() {
|
|
320
|
+
redo(this.view);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
toggleWordWrap(status=undefined) {
|
|
325
|
+
if (status===undefined)
|
|
326
|
+
status = !this.isWordWrapped();
|
|
327
|
+
|
|
328
|
+
this.view.dispatch({ // Update codemirror.
|
|
329
|
+
effects: this.compartments.lineWrapping.reconfigure(status ? EditorView.lineWrapping : [])
|
|
330
|
+
});
|
|
331
|
+
this.wordWrap = status;
|
|
332
|
+
|
|
333
|
+
if (this.toolbar)
|
|
334
|
+
this.toolbar.update();
|
|
335
|
+
|
|
336
|
+
this.onOptionChange();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
isWordWrapped() {
|
|
340
|
+
return this.wordWrap;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
toggleSearchPanel(status=undefined) {
|
|
344
|
+
if (status===undefined)
|
|
345
|
+
status = !searchPanelOpen(this.view.state);
|
|
346
|
+
|
|
347
|
+
if (status) {
|
|
348
|
+
openSearchPanel(this.view)
|
|
349
|
+
|
|
350
|
+
// Make search panel styles a little closer to SiteCrafter styles.
|
|
351
|
+
for (let btn of this.querySelectorAll('.cm-search .cm-button'))
|
|
352
|
+
btn.classList.replace('cm-button', 'button')
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
closeSearchPanel(this.view)
|
|
356
|
+
if (this.toolbar)
|
|
357
|
+
this.toolbar.update();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
isSearchPanelOpen() {
|
|
361
|
+
return this.view && searchPanelOpen(this.view.state);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Used internally. Use setValue() to change the language. */
|
|
366
|
+
getLanguageExtension(language, languageConfig) {
|
|
367
|
+
this.language = language;
|
|
368
|
+
|
|
369
|
+
// TODO: It may be possible to provide additional style overrides, e.g. for css tag selectors:
|
|
370
|
+
// https://discuss.codemirror.net/t/highlighting-markdown-mark-only/3964/3
|
|
371
|
+
|
|
372
|
+
let langExt;
|
|
373
|
+
if (['css', 'scss', 'less'].includes(language))
|
|
374
|
+
langExt = css;
|
|
375
|
+
else if (['htm', 'html'].includes(language))
|
|
376
|
+
langExt = ParseUtil.htmlWithJavaScript;
|
|
377
|
+
else if (['js', 'ts', 'jsx', 'tsx', 'jscript', 'javascript'].includes(language))
|
|
378
|
+
langExt = ParseUtil.javascriptWithHtml; // TODO: Support it inside html and php
|
|
379
|
+
else if (language === 'json')
|
|
380
|
+
langExt = json;
|
|
381
|
+
// else if (language === 'ts') // TODO: jsx, tsx, python
|
|
382
|
+
// langExt = typescript();
|
|
383
|
+
else if (['md', 'markdown'].includes(language))
|
|
384
|
+
langExt = markdown;
|
|
385
|
+
else if (language === 'php')
|
|
386
|
+
langExt = ParseUtil.phpWithHtmlWithJavaScript;
|
|
387
|
+
else if (language === 'php-plain')
|
|
388
|
+
langExt = php; // Use this to default into php mode without an opening <? tag.
|
|
389
|
+
else if (language === 'sql')
|
|
390
|
+
langExt = sql;
|
|
391
|
+
else if (['py', 'python'].includes(language))
|
|
392
|
+
langExt = python;
|
|
393
|
+
if (!langExt)
|
|
394
|
+
return []; // plain text.
|
|
395
|
+
|
|
396
|
+
return [
|
|
397
|
+
langExt(languageConfig)
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
*
|
|
403
|
+
* @param range {CodeRange}*/
|
|
404
|
+
async selectRange(range) {
|
|
405
|
+
range = range.clone();
|
|
406
|
+
if (range.start.line === null) {
|
|
407
|
+
await asyncDispatch(this.view, {
|
|
408
|
+
selection: { // Deselect all.
|
|
409
|
+
head: 0,
|
|
410
|
+
anchor: 0
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
let doc = this.view.state.doc;
|
|
418
|
+
|
|
419
|
+
// Keep within bounds.
|
|
420
|
+
if (doc.lines < range.start.line)
|
|
421
|
+
range.start.line = doc.lines-1;
|
|
422
|
+
if (doc.lines < range.end.line)
|
|
423
|
+
range.end.line = doc.lines-1;
|
|
424
|
+
if (range.end.line === null)
|
|
425
|
+
range.end.line = range.start.line;
|
|
426
|
+
|
|
427
|
+
// Sometimes these can be out of bounds if the line is pair of comments for a php print.
|
|
428
|
+
// In that case we just select the whole line for now.
|
|
429
|
+
if (doc.line(range.start.line+1).length < range.start.column)
|
|
430
|
+
range.start.column = 0; //doc.text[range.start.line].length
|
|
431
|
+
if (doc.line(range.end.line+1).length < range.end.column)
|
|
432
|
+
range.end.column = doc.line(range.end.line+1).length;
|
|
433
|
+
|
|
434
|
+
const startLineObj = doc.line(range.start.line+1);
|
|
435
|
+
const endLineObj = doc.line(range.end.line+1);
|
|
436
|
+
|
|
437
|
+
this.allowChanges = false; // Don't call onSelectionChange()
|
|
438
|
+
|
|
439
|
+
await asyncDispatch(this.view, {
|
|
440
|
+
// Set selection to that entire line.
|
|
441
|
+
selection: {
|
|
442
|
+
head: startLineObj.from + range.start.column,
|
|
443
|
+
anchor: range.end.column === null
|
|
444
|
+
? endLineObj.to // If no end column, select until end of line.
|
|
445
|
+
: endLineObj.from + range.end.column
|
|
446
|
+
},
|
|
447
|
+
// Ensure the selection is shown in viewport
|
|
448
|
+
scrollIntoView: true
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
this.allowChanges = true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
setLanguage(language=null, languageConfig=null) {
|
|
455
|
+
languageConfig = languageConfig || {};
|
|
456
|
+
this.view.dispatch({ // Update codemirror.
|
|
457
|
+
effects: this.compartments.language.reconfigure(this.getLanguageExtension(language, languageConfig))
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
setTabSize(tabSize) {
|
|
462
|
+
this.tabSize = tabSize;
|
|
463
|
+
this.view.dispatch({ // Update codemirror.
|
|
464
|
+
effects: this.compartments.tabSize.reconfigure(EditorState.tabSize.of(tabSize))
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* TODO: Can't change language if createHistory=false */
|
|
470
|
+
async setValue(value, createHistory=true) {
|
|
471
|
+
let scroller = this.querySelector('.cm-editor > .cm-scroller');
|
|
472
|
+
let scroll = {x: scroller?.scrollLeft||0, y: scroller?.scrollTop||0};
|
|
473
|
+
|
|
474
|
+
// Don't trigger the onChange function
|
|
475
|
+
this.allowChanges = false;
|
|
476
|
+
|
|
477
|
+
// https://codemirror.net/docs/migration/
|
|
478
|
+
if (createHistory)
|
|
479
|
+
await asyncDispatch(this.view, { // Also triggers the change event.
|
|
480
|
+
changes: {from: 0, to: this.view.state.doc.length, insert: value}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// https://discuss.codemirror.net/t/codemirror-6-cm-clearhistory-equivalent/2851/2
|
|
484
|
+
else {
|
|
485
|
+
this.view.setState(EditorState.create({
|
|
486
|
+
doc: value,
|
|
487
|
+
extensions: this.getExtensions(this.wordWrap)
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.allowChanges = true;
|
|
492
|
+
|
|
493
|
+
// if (!triggerChange)
|
|
494
|
+
// this.allowChanges = true;
|
|
495
|
+
|
|
496
|
+
// Not sure why both of these are necessary, but in my testing they were.
|
|
497
|
+
if (scroller) {
|
|
498
|
+
scroller.scrollTop = scroll.y;
|
|
499
|
+
scroller.scrollLeft = scroll.x;
|
|
500
|
+
requestAnimationFrame(() => {
|
|
501
|
+
scroller.scrollTop = scroll.y;
|
|
502
|
+
scroller.scrollLeft = scroll.x;
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
getUndoDepth() {
|
|
508
|
+
if (this.view)
|
|
509
|
+
return undoDepth(this.view.state);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
getRedoDepth() {
|
|
513
|
+
if (this.view)
|
|
514
|
+
return redoDepth(this.view.state);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
render() {
|
|
519
|
+
this.html = r`
|
|
520
|
+
<code-editor>
|
|
521
|
+
<style>
|
|
522
|
+
:host { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0 }
|
|
523
|
+
:host [data-id=editor] { height: 100%; width: 100%; min-height: 0 }
|
|
524
|
+
:host [data-id=editor] .cm-editor { height: 100%; width: 100%; min-height: 0 }
|
|
525
|
+
:host .cm-scroller { overflow: auto; min-height: 10px; height: 100%; width: 100%; font: 13px Hack, monospace !important }
|
|
526
|
+
|
|
527
|
+
/* Remove outline added by CodeMirror on focus. */
|
|
528
|
+
:host [data-id=editor] .cm-editor.cm-focused { outline: none }
|
|
529
|
+
:host .cm-activeLineGutter { background: transparent} /* Only highlight on focus, set in the theme.js file */
|
|
530
|
+
|
|
531
|
+
/* Search Panel styles */
|
|
532
|
+
:host .cm-panels.cm-panels-bottom { border-top: var(--border, 1px solid #cbcfd7) }
|
|
533
|
+
:host .cm-panels { background-color: var(--background, white) !important; color: var(--text, #333) !important }
|
|
534
|
+
:host .cm-panels label { display: inline-flex; align-items: center; user-select: none }
|
|
535
|
+
:host .cm-textfield { font-size: inherit }
|
|
536
|
+
</style>
|
|
537
|
+
<div data-id="editor" class="col"></div>
|
|
538
|
+
</code-editor>`
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
CodeEditor.define();
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
// Define a unique effect to listen for
|
|
548
|
+
const asyncEffect = StateEffect.define();
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Use this function to allow awaiting a CodeMirror state.dispatch() call.
|
|
554
|
+
* @param view
|
|
555
|
+
* @param options
|
|
556
|
+
* @returns {Promise<unknown>}
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* await asyncDispatch(this.view, dispatchArgs); */
|
|
560
|
+
async function asyncDispatch(view, options) {
|
|
561
|
+
//view.dispatch(options); // non-async version
|
|
562
|
+
return new Promise(resolve => {
|
|
563
|
+
options.effects = asyncEffect.of(resolve);
|
|
564
|
+
view.dispatch(options);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
var Extensions = {
|
|
570
|
+
|
|
571
|
+
// Create a state field that listens for your effect
|
|
572
|
+
asyncField: StateField.define({
|
|
573
|
+
create() {
|
|
574
|
+
return {}; // Initial state (can be anything relevant)
|
|
575
|
+
},
|
|
576
|
+
update(value, tr, a) {
|
|
577
|
+
if (tr.effects.some(e => e.is(asyncEffect))) {
|
|
578
|
+
// Supposedly called after dispatched event is done, but doesn't seem to be?
|
|
579
|
+
let resolve = tr.effects[0].value; // arg sent to asncEffect.of() in asyncDispatch()
|
|
580
|
+
resolve();
|
|
581
|
+
}
|
|
582
|
+
return value; // Return the updated state
|
|
583
|
+
}
|
|
584
|
+
}),
|
|
585
|
+
|
|
586
|
+
// Call onChange() when the document changes.
|
|
587
|
+
// https://discuss.codemirror.net/t/how-to-listen-to-changes-for-react-controlled-component/4506/4
|
|
588
|
+
changeDetection(codeEditor) {
|
|
589
|
+
return EditorView.updateListener.of(update => {
|
|
590
|
+
if (update.docChanged || update.selectionSet)
|
|
591
|
+
codeEditor.onInternalChange(update, codeEditor);
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
var ParseUtil = {
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* A langauge extension for CodeMirror to support highlighting nested javascript/html inside javascript templates.
|
|
600
|
+
* https://codemirror.net/examples/mixed-language/
|
|
601
|
+
* @return {LRLanguage} */
|
|
602
|
+
javascriptWithHtml: () => LRLanguage.define({parser: ParseUtil.jsParser}),
|
|
603
|
+
htmlWithJavaScript: () => LRLanguage.define({parser: ParseUtil.htmlParser}),
|
|
604
|
+
phpWithHtmlWithJavaScript: () => LRLanguage.define({parser: ParseUtil.phpParser}),
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Used by javascriptWithHtml() */
|
|
608
|
+
htmlParser: htmlParser.configure({
|
|
609
|
+
wrap: parseMixed((node, docInput) => {
|
|
610
|
+
if (node.name == "StyleText")
|
|
611
|
+
return {parser: cssParser}
|
|
612
|
+
if (node.name == "ScriptText")
|
|
613
|
+
return {parser: ParseUtil.jsParser}
|
|
614
|
+
|
|
615
|
+
// Use javascript parser for template expressions
|
|
616
|
+
if (node.type.name === 'Document' && docInput.doc) {
|
|
617
|
+
let code = docInput.doc.sliceString(node.from, node.to);
|
|
618
|
+
let overlay = [];
|
|
619
|
+
code.replace(/\${/g, (a, startIndex, c) => {
|
|
620
|
+
let endIndex = ParseUtil.findMatchingBrace(code, startIndex+2);
|
|
621
|
+
if (endIndex !== -1)
|
|
622
|
+
overlay.push({from: node.from + startIndex, to: node.from + endIndex + 1});
|
|
623
|
+
})
|
|
624
|
+
return {
|
|
625
|
+
parser: ParseUtil.jsParser,
|
|
626
|
+
overlay
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
})
|
|
630
|
+
}),
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Used by javascriptWithHtml() */
|
|
634
|
+
jsParser: jsParser.configure({
|
|
635
|
+
wrap: parseMixed((node, docInput) => {
|
|
636
|
+
if (node.name == "TemplateString") {
|
|
637
|
+
let code = docInput.doc.sliceString(node.from, node.to);
|
|
638
|
+
|
|
639
|
+
// Only if node text starts and ends with tags, allowing for spaces.
|
|
640
|
+
if (code.match(/^`\s*</) && code.match(/>\s*`$/))
|
|
641
|
+
return {parser: ParseUtil.htmlParser}
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
})
|
|
645
|
+
}),
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Replaces CodeMirror's php() function with one that uses the htmlParser above.
|
|
649
|
+
* This lets us syntax highlight html in javascript template tags.
|
|
650
|
+
* Search codemirror6.js for "PHP language support"
|
|
651
|
+
* to find the original function this is copied from. */
|
|
652
|
+
phpParser: phpParser.configure({
|
|
653
|
+
wrap: parseMixed(node => {
|
|
654
|
+
if (!node.type.isTop)
|
|
655
|
+
return null;
|
|
656
|
+
return {
|
|
657
|
+
parser: ParseUtil.htmlParser,
|
|
658
|
+
overlay: node => node.name == "Text"
|
|
659
|
+
};
|
|
660
|
+
}),
|
|
661
|
+
top: "Template"
|
|
662
|
+
|
|
663
|
+
}),
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Used by javascriptWithHtml()
|
|
668
|
+
* @param str {string}
|
|
669
|
+
* @param startIndex {int}
|
|
670
|
+
* @returns {int} -1 if no match. */
|
|
671
|
+
findMatchingBrace(str, startIndex) {
|
|
672
|
+
let stack = 0;
|
|
673
|
+
let inString = null;
|
|
674
|
+
let inComment = null;
|
|
675
|
+
|
|
676
|
+
for (let i=startIndex; i<str.length; i++) {
|
|
677
|
+
let char = str[i];
|
|
678
|
+
if (inString && char === '\\') { // Skip escaped characters in strings.
|
|
679
|
+
i++;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
let nextChar = i + 1 < str.length ? str[i+1] : '';
|
|
683
|
+
|
|
684
|
+
// String start/end
|
|
685
|
+
if (char === '`' && !inComment)
|
|
686
|
+
inString = inString ? null : '`';
|
|
687
|
+
else if ((char === '"' || char === "'") && !inComment)
|
|
688
|
+
inString = inString === char ? null : char;
|
|
689
|
+
|
|
690
|
+
// Comment start/end
|
|
691
|
+
if (!inString && char === '/' && (nextChar === '*' || nextChar === '/')) {
|
|
692
|
+
inComment = nextChar === '*' ? 'block' : 'line';
|
|
693
|
+
i++; // Skip the next character as it's part of the comment syntax
|
|
694
|
+
}
|
|
695
|
+
else if (inComment === 'block' && char === '*' && nextChar === '/') {
|
|
696
|
+
inComment = null;
|
|
697
|
+
i++; // Skip the '/' character
|
|
698
|
+
}
|
|
699
|
+
else if (inComment === 'line' && char === '\n')
|
|
700
|
+
inComment = null;
|
|
701
|
+
|
|
702
|
+
// Template literal boundaries
|
|
703
|
+
if (!inString && !inComment) {
|
|
704
|
+
if (char === '$' && nextChar === '{') {
|
|
705
|
+
stack++;
|
|
706
|
+
i++; // Skip the '{' character
|
|
707
|
+
} else if (char === '}') {
|
|
708
|
+
if (stack === 0)
|
|
709
|
+
return i; // Matching closing brace found
|
|
710
|
+
stack--;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return -1;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* @typedef ToolbarButton
|
|
722
|
+
* @property {string} html
|
|
723
|
+
* @property {function(el:Node|HTMLElement)} update */
|
|
724
|
+
export class CodeEditorToolbar extends Solarite {
|
|
725
|
+
|
|
726
|
+
/** @type {object} Should have functions for everything the buttons array uses. */
|
|
727
|
+
ed;
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Internal representation of buttons.
|
|
731
|
+
* @type {{html:string, update:function}[]} */
|
|
732
|
+
buttons = [];
|
|
733
|
+
|
|
734
|
+
buttonTemplates
|
|
735
|
+
|
|
736
|
+
/** @type {HTMLElement} */
|
|
737
|
+
buttonEls;
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* @param ed {CodeEditor}
|
|
741
|
+
* @param buttons {(string|ToolbarButton)[]} Names of built-in buttons to use, or objects to define new buttons */
|
|
742
|
+
constructor({ed, buttons=[]}={}) {
|
|
743
|
+
super();
|
|
744
|
+
this.ed = ed;
|
|
745
|
+
|
|
746
|
+
this.buttonTemplates = {
|
|
747
|
+
'|': {
|
|
748
|
+
html: r`<span>|</span>`
|
|
749
|
+
},
|
|
750
|
+
undo: {
|
|
751
|
+
html: r`<button onclick="${e => this.ed.undo()}" class="flat" title="Undo">${r(Icons.undo)}</button>`,
|
|
752
|
+
update: (el, sel) => el.toggleAttribute('disabled', this.ed.getUndoDepth() === 0)
|
|
753
|
+
},
|
|
754
|
+
redo: {
|
|
755
|
+
html: r`<button onclick="${e => this.ed.redo()}" class="flat" title="Redo">${r(Icons.redo)}</button>`,
|
|
756
|
+
update: (el, sel) => el.toggleAttribute('disabled', this.ed.getRedoDepth() === 0)
|
|
757
|
+
},
|
|
758
|
+
wordWrap: {
|
|
759
|
+
html: r`<button onclick="${e => this.ed.toggleWordWrap()}" class="flat ${ed.isWordWrapped() && 'selected'}" title="Word Wrap">${r(Icons.wordWrap)}</button>`,
|
|
760
|
+
update: (el, sel) => el.classList.toggle('selected', this.ed.isWordWrapped())
|
|
761
|
+
},
|
|
762
|
+
run: {
|
|
763
|
+
html: r`<button onclick="${e => this.ed.run()}" data-id="btnRun" title="Ctrl+Enter" class="flat">${r(Icons.triangleRight)}Run</button>`
|
|
764
|
+
},
|
|
765
|
+
findReplace: {
|
|
766
|
+
html: r`<button onclick="${e => this.ed.toggleSearchPanel()}" class="flat" title="Find and replace (ctrl+f)">${r(Icons.findReplace)}</button>`,
|
|
767
|
+
update: (el, sel) => el.classList.toggle('selected', this.ed.isSearchPanelOpen())
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
if (typeof buttons === 'string')
|
|
772
|
+
buttons = buttons.split(/[,\s]+/g);
|
|
773
|
+
|
|
774
|
+
for (let button of buttons||[]) {
|
|
775
|
+
let item = typeof button === 'string'
|
|
776
|
+
? this.buttonTemplates[button]
|
|
777
|
+
: button;
|
|
778
|
+
if (!item)
|
|
779
|
+
throw new Error('Invalid button ' + button);
|
|
780
|
+
this.buttons.push(item);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
this.render();
|
|
784
|
+
|
|
785
|
+
Util.on(this, 'mousedown', '[data-id=buttonEls] .icon', e => {
|
|
786
|
+
let btn = e.target.closest('.icon');
|
|
787
|
+
|
|
788
|
+
if (btn.hasAttribute('disabled')) {
|
|
789
|
+
e.stopPropagation();
|
|
790
|
+
e.stopImmediatePropagation();
|
|
791
|
+
e.preventDefault();
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
this.update();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
command(e, func) {
|
|
800
|
+
e.preventDefault(); // Don't lose focus
|
|
801
|
+
this.ed[func]();
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Update toolbar buttons depending on the editor state*/
|
|
806
|
+
update() {
|
|
807
|
+
for (let i in this.buttons) {
|
|
808
|
+
let button = this.buttons[i];
|
|
809
|
+
if (button.update)
|
|
810
|
+
button.update(this.buttonEls.children[i], this);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
render() {
|
|
815
|
+
this.html = r`
|
|
816
|
+
<code-editor-toolbar>
|
|
817
|
+
<style>
|
|
818
|
+
:host { display: block }
|
|
819
|
+
:host [data-id='buttonEls'] span { user-select: none }
|
|
820
|
+
:host .blockSelect [data-id='dropdown'] > div span { opacity: .5 } /* Hotkey text */
|
|
821
|
+
</style>
|
|
822
|
+
|
|
823
|
+
<div data-id="buttonEls" class="row wrap center-v toolbar">
|
|
824
|
+
${this.buttons.map(button => button.html || button)}
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<!--
|
|
828
|
+
Run
|
|
829
|
+
Word wrap
|
|
830
|
+
format
|
|
831
|
+
find/replace
|
|
832
|
+
undo/redo
|
|
833
|
+
show line numbers
|
|
834
|
+
-->
|
|
835
|
+
</code-editor-toolbar>`
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
}
|
|
839
|
+
CodeEditorToolbar.define();
|
|
840
|
+
|