lexgui 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.
@@ -0,0 +1,1865 @@
1
+ import { LX } from 'lexgui';
2
+
3
+ if(!LX) {
4
+ throw("lexgui.js missing!");
5
+ }
6
+
7
+ LX.components.push( 'CodeEditor' );
8
+
9
+ function flushCss(element) {
10
+ // By reading the offsetHeight property, we are forcing
11
+ // the browser to flush the pending CSS changes (which it
12
+ // does to ensure the value obtained is accurate).
13
+ element.offsetHeight;
14
+ }
15
+
16
+ function swapElements (obj, a, b) {
17
+ [obj[a], obj[b]] = [obj[b], obj[a]];
18
+ }
19
+
20
+ function swapArrayElements (array, id0, id1) {
21
+ [array[id0], array[id1]] = [array[id1], array[id0]];
22
+ };
23
+
24
+ function sliceChar(str, idx) {
25
+ return str.substr(0, idx) + str.substr(idx + 1);
26
+ }
27
+
28
+ function firstNonspaceIndex(str) {
29
+ return str.search(/\S|$/);
30
+ }
31
+
32
+ let ASYNC_ENABLED = true;
33
+
34
+ function doAsync( fn, ms ) {
35
+ if( ASYNC_ENABLED )
36
+ setTimeout( fn, ms ?? 0 );
37
+ else
38
+ fn();
39
+ }
40
+
41
+ class ISelection {
42
+
43
+ constructor(editor, ix, iy) {
44
+
45
+ this.editor = editor;
46
+ this.chars = 0;
47
+
48
+ this.fromX = ix;
49
+ this.toX = ix;
50
+ this.fromY = iy;
51
+ this.toY = iy;
52
+ }
53
+
54
+ sameLine() {
55
+ return this.fromY === this.toY;
56
+ }
57
+
58
+ invertIfNecessary() {
59
+ if(this.fromX > this.toX)
60
+ swapElements(this, 'fromX', 'toX');
61
+ if(this.fromY > this.toY)
62
+ swapElements(this, 'fromY', 'toY');
63
+ }
64
+
65
+ selectInline(x, y, width) {
66
+
67
+ this.chars = width / this.editor.charWidth;
68
+ this.fromX = x;
69
+ this.toX = x + this.chars;
70
+ this.fromY = this.toY = y;
71
+
72
+ var domEl = document.createElement('div');
73
+ domEl.className = "lexcodeselection";
74
+
75
+ domEl._top = 4 + y * this.editor.lineHeight;
76
+ domEl.style.top = (domEl._top - this.editor.getScrollTop()) + "px";
77
+ domEl._left = x * this.editor.charWidth;
78
+ domEl.style.left = "calc(" + (domEl._left - this.editor.getScrollLeft()) + "px + " + this.editor.xPadding + ")";
79
+ domEl.style.width = width + "px";
80
+ this.editor.selections.appendChild(domEl);
81
+ }
82
+ };
83
+
84
+ /**
85
+ * @class CodeEditor
86
+ */
87
+
88
+ class CodeEditor {
89
+
90
+ static __instances = [];
91
+
92
+ static CURSOR_LEFT = 1;
93
+ static CURSOR_TOP = 2;
94
+
95
+ /**
96
+ * @param {*} options
97
+ * skip_info, allow_add_scripts, name
98
+ */
99
+
100
+ constructor( area, options = {} ) {
101
+
102
+ CodeEditor.__instances.push( this );
103
+
104
+ this.disable_edition = options.disable_edition ?? false;
105
+
106
+ this.skip_info = options.skip_info;
107
+ this.base_area = area;
108
+ this.area = new LX.Area( { className: "lexcodeeditor", height: "auto", no_append: true } );
109
+
110
+ this.tabs = this.area.addTabs( { onclose: (name) => delete this.openedTabs[name] } );
111
+ this.tabs.root.addEventListener( 'dblclick', (e) => {
112
+ if( options.allow_add_scripts ?? true ) {
113
+ e.preventDefault();
114
+ this.addTab("unnamed.js", true);
115
+ }
116
+ } );
117
+
118
+ area.root.classList.add('codebasearea');
119
+ this.gutter = document.createElement('div');
120
+ this.gutter.className = "lexcodegutter";
121
+ area.attach( this.gutter );
122
+
123
+ this.root = this.area.root;
124
+ this.root.tabIndex = -1;
125
+ area.attach( this.root );
126
+
127
+ this.root.addEventListener( 'keydown', this.processKey.bind(this), true);
128
+ this.root.addEventListener( 'mousedown', this.processMouse.bind(this) );
129
+ this.root.addEventListener( 'mouseup', this.processMouse.bind(this) );
130
+ this.root.addEventListener( 'mousemove', this.processMouse.bind(this) );
131
+ this.root.addEventListener( 'click', this.processMouse.bind(this) );
132
+ this.root.addEventListener( 'contextmenu', this.processMouse.bind(this) );
133
+ this.root.addEventListener( 'focus', this.processFocus.bind(this, true) );
134
+ this.root.addEventListener( 'focusout', this.processFocus.bind(this, false) );
135
+
136
+ // Cursors and selection
137
+
138
+ this.cursors = document.createElement('div');
139
+ this.cursors.className = 'cursors';
140
+ this.tabs.area.attach(this.cursors);
141
+
142
+ this.selections = document.createElement('div');
143
+ this.selections.className = 'selections';
144
+ this.tabs.area.attach(this.selections);
145
+
146
+ // Css char synchronization
147
+ this.xPadding = "0.25em";
148
+
149
+ // Add main cursor
150
+ {
151
+ var cursor = document.createElement('div');
152
+ cursor.className = "cursor";
153
+ cursor.innerHTML = " ";
154
+ cursor._left = 0;
155
+ cursor.style.left = this.xPadding;
156
+ cursor._top = 4;
157
+ cursor.style.top = "4px";
158
+ cursor.position = 0;
159
+ cursor.line = 0;
160
+ this.cursors.appendChild(cursor);
161
+ }
162
+
163
+ // State
164
+
165
+ this.state = {
166
+ overwrite: false,
167
+ focused: false,
168
+ selectingText: false
169
+ }
170
+
171
+ // Code
172
+
173
+ this.highlight = options.highlight ?? 'Plain Text';
174
+ this.onsave = options.onsave ?? ((code) => { });
175
+ this.onrun = options.onrun ?? ((code) => { this.runScript(code) });
176
+ this.actions = {};
177
+ this.cursorBlinkRate = 550;
178
+ this.tabSpaces = 4;
179
+ this.maxUndoSteps = 16;
180
+ this.lineHeight = 22;
181
+ this.charWidth = 9;//this.measureChar();
182
+ this._lastTime = null;
183
+
184
+ this.languages = [
185
+ 'Plain Text', 'JavaScript', 'CSS', 'GLSL', 'WGSL', 'JSON', 'XML'
186
+ ];
187
+ this.specialKeys = [
188
+ 'Backspace', 'Enter', 'ArrowUp', 'ArrowDown',
189
+ 'ArrowRight', 'ArrowLeft', 'Delete', 'Home',
190
+ 'End', 'Tab'
191
+ ];
192
+ this.keywords = {
193
+ 'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof'],
194
+ 'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
195
+ 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
196
+ 'WGSL': ['var', 'const', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
197
+ 'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
198
+ 'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
199
+ 'texture_storage_2d_array', 'texture_storage_3d'],
200
+ };
201
+ this.builtin = {
202
+ 'JavaScript': ['console', 'window', 'navigator'],
203
+ 'CSS': ['*', '!important']
204
+ };
205
+ this.literals = {
206
+ 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do'],
207
+ 'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
208
+ 'WGSL': ['for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'uniform']
209
+ };
210
+ this.symbols = {
211
+ 'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
212
+ 'JSON': ['[', ']', '{', '}', '(', ')'],
213
+ 'GLSL': ['[', ']', '{', '}', '(', ')'],
214
+ 'WGSL': ['[', ']', '{', '}', '(', ')', '->'],
215
+ 'CSS': ['{', '}', '(', ')', '*']
216
+ };
217
+
218
+ // Action keys
219
+
220
+ this.action('Backspace', false, ( ln, cursor, e ) => {
221
+
222
+ this._addUndoStep(cursor);
223
+
224
+ if(this.selection) {
225
+ this.deleteSelection(cursor);
226
+ // Remove entire line when selecting with triple click
227
+ if(this.code.lines[ln] && !this.code.lines[ln].length)
228
+ this.actions['Backspace'].callback(ln, cursor, e);
229
+ }
230
+ else {
231
+ var letter = this.getCharAtPos( cursor, -1 );
232
+ if(letter) {
233
+ this.code.lines[ln] = sliceChar( this.code.lines[ln], cursor.position - 1 );
234
+ this.cursorToLeft( letter );
235
+ this.processLine(ln);
236
+ }
237
+ else if(this.code.lines[ln - 1] != undefined) {
238
+ this.lineUp();
239
+ this.actions['End'].callback(cursor.line, cursor, e);
240
+ // Move line on top
241
+ this.code.lines[ln - 1] += this.code.lines[ln];
242
+ this.code.lines.splice(ln, 1);
243
+ this.processLines(ln - 1);
244
+ }
245
+ }
246
+ });
247
+
248
+ this.action('Delete', false, ( ln, cursor, e ) => {
249
+
250
+ this._addUndoStep( cursor );
251
+
252
+ if(this.selection) {
253
+ // Use 'Backspace' as it's the same callback...
254
+ this.actions['Backspace'].callback(ln, cursor, e);
255
+ }
256
+ else
257
+ {
258
+ var letter = this.getCharAtPos( cursor );
259
+ if(letter) {
260
+ this.code.lines[ln] = sliceChar( this.code.lines[ln], cursor.position );
261
+ this.processLine(ln);
262
+ }
263
+ else if(this.code.lines[ln + 1] != undefined) {
264
+ this.code.lines[ln] += this.code.lines[ln + 1];
265
+ this.code.lines.splice(ln + 1, 1);
266
+ this.processLines(ln);
267
+ }
268
+ }
269
+ });
270
+
271
+ this.action('Tab', true, ( ln, cursor, e ) => {
272
+ this.addSpaces( this.tabSpaces );
273
+ });
274
+
275
+ this.action('Home', false, ( ln, cursor, e ) => {
276
+
277
+ let idx = firstNonspaceIndex(this.code.lines[ln]);
278
+
279
+ // We already are in the first non space index...
280
+ if(idx == cursor.position) idx = 0;
281
+
282
+ const prestring = this.code.lines[ln].substring(0, idx);
283
+ let last_pos = cursor.position;
284
+
285
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
286
+ if(idx > 0) this.cursorToString(cursor, prestring);
287
+ this._refresh_code_info(cursor.line + 1, cursor.position);
288
+ this.code.scrollLeft = 0;
289
+
290
+ if( !e.shiftKey || e.cancelShift )
291
+ return;
292
+
293
+ // Get last selection range
294
+ if(this.selection)
295
+ last_pos += this.selection.chars;
296
+
297
+ this.startSelection(cursor);
298
+ var string = this.code.lines[ln].substring(idx, last_pos);
299
+ this.selection.selectInline(idx, cursor.line, this.measureString(string));
300
+ });
301
+
302
+ this.action('End', false, ( ln, cursor, e ) => {
303
+
304
+ if( e.shiftKey || e._shiftKey ) {
305
+
306
+ var string = this.code.lines[ln].substring(cursor.position);
307
+ if(!this.selection)
308
+ this.startSelection(cursor);
309
+ this.selection.selectInline(cursor.position, cursor.line, this.measureString(string));
310
+ }
311
+
312
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
313
+ this.cursorToString( cursor, this.code.lines[ln] );
314
+
315
+ const last_char = ((this.code.scrollLeft + this.code.clientWidth) / this.charWidth)|0;
316
+ this.code.scrollLeft = cursor.position >= last_char ? (cursor.position - last_char) * this.charWidth : 0;
317
+ });
318
+
319
+ this.action('Enter', true, ( ln, cursor, e ) => {
320
+
321
+ if(e.ctrlKey)
322
+ {
323
+ this.onrun( this.getText() );
324
+ return;
325
+ }
326
+
327
+ this._addUndoStep(cursor);
328
+
329
+ var _c0 = this.getCharAtPos( cursor, -1 );
330
+ var _c1 = this.getCharAtPos( cursor );
331
+
332
+ this.code.lines.splice(cursor.line + 1, 0, "");
333
+ this.code.lines[cursor.line + 1] = this.code.lines[ln].substr( cursor.position ); // new line (below)
334
+ this.code.lines[ln] = this.code.lines[ln].substr( 0, cursor.position ); // line above
335
+ this.lineDown(cursor, true);
336
+
337
+ // Check indentation
338
+ var spaces = firstNonspaceIndex(this.code.lines[ln]);
339
+ var tabs = Math.floor( spaces / this.tabSpaces );
340
+
341
+ if( _c0 == '{' && _c1 == '}' ) {
342
+ this.code.lines.splice(cursor.line, 0, "");
343
+ this.addSpaceTabs(tabs + 1);
344
+ this.code.lines[cursor.line + 1] = " ".repeat(spaces) + this.code.lines[cursor.line + 1];
345
+ } else {
346
+ this.addSpaceTabs(tabs);
347
+ }
348
+
349
+ this.processLines( ln );
350
+ });
351
+
352
+ this.action('ArrowUp', false, ( ln, cursor, e ) => {
353
+ if(e.shiftKey) {
354
+ if(!this.selection)
355
+ this.startSelection(cursor);
356
+ this.selection.fromX = cursor.position;
357
+ this.selection.toY = this.selection.toY > 0 ? this.selection.toY - 1 : 0;
358
+ this.processSelection(null, true);
359
+ this.cursorToPosition(cursor, this.selection.fromX);
360
+ this.cursorToLine(cursor, this.selection.toY);
361
+ } else {
362
+ this.endSelection();
363
+ this.lineUp();
364
+ // Go to end of line if out of line
365
+ var letter = this.getCharAtPos( cursor );
366
+ if(!letter) this.actions['End'].callback(cursor.line, cursor, e);
367
+ }
368
+ });
369
+
370
+ this.action('ArrowDown', false, ( ln, cursor, e ) => {
371
+ if(e.shiftKey) {
372
+ if(!this.selection)
373
+ this.startSelection(cursor);
374
+ this.selection.toX = cursor.position;
375
+ this.selection.toY = this.selection.toY < this.code.lines.length - 1 ? this.selection.toY + 1 : this.code.lines.length - 1;
376
+ this.processSelection(null, true);
377
+ this.cursorToPosition(cursor, this.selection.toX);
378
+ this.cursorToLine(cursor, this.selection.toY);
379
+ } else {
380
+
381
+ if( this.code.lines[ ln + 1 ] == undefined )
382
+ return;
383
+ this.endSelection();
384
+ this.lineDown();
385
+ // Go to end of line if out of line
386
+ var letter = this.getCharAtPos( cursor );
387
+ if(!letter) this.actions['End'].callback(cursor.line, cursor, e);
388
+ }
389
+ });
390
+
391
+ this.action('ArrowLeft', false, ( ln, cursor, e ) => {
392
+
393
+ if(e.metaKey) { // Apple devices (Command)
394
+ e.preventDefault();
395
+ this.actions[ 'Home' ].callback( ln, cursor );
396
+ }
397
+ else if(e.ctrlKey) {
398
+ // Get next word
399
+ const [word, from, to] = this.getWordAtPos( cursor, -1 );
400
+ var diff = Math.max(cursor.position - from, 1);
401
+ var substr = word.substr(0, diff);
402
+ // Selections...
403
+ if( e.shiftKey ) if(!this.selection) this.startSelection(cursor);
404
+ this.cursorToString(cursor, substr, true);
405
+ if( e.shiftKey ) this.processSelection();
406
+ }
407
+ else {
408
+ var letter = this.getCharAtPos( cursor, -1 );
409
+ if(letter) {
410
+ if( e.shiftKey ) {
411
+ if(!this.selection) this.startSelection(cursor);
412
+ if( ((cursor.position - 1) < this.selection.fromX) && this.selection.sameLine() )
413
+ this.selection.fromX--;
414
+ else if( (cursor.position - 1) == this.selection.fromX && this.selection.sameLine() ) {
415
+ this.cursorToLeft( letter, cursor );
416
+ this.endSelection();
417
+ return;
418
+ }
419
+ else this.selection.toX--;
420
+ this.cursorToLeft( letter, cursor );
421
+ this.processSelection(null, true);
422
+ }
423
+ else {
424
+ if(!this.selection) this.cursorToLeft( letter, cursor );
425
+ else {
426
+ this.selection.invertIfNecessary();
427
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
428
+ this.cursorToLine(cursor, this.selection.fromY, true);
429
+ this.cursorToPosition(cursor, this.selection.fromX);
430
+ this.endSelection();
431
+ }
432
+ }
433
+ }
434
+ else if( cursor.line > 0 ) {
435
+
436
+ this.lineUp( cursor );
437
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
438
+ this.cursorToPosition( cursor, this.code.lines[cursor.line].length );
439
+
440
+ if( e.shiftKey ) {
441
+ if(!this.selection) this.startSelection(cursor);
442
+ this.selection.toX = cursor.position;
443
+ this.selection.toY--;
444
+ this.processSelection(null, true);
445
+ }
446
+ }
447
+ }
448
+ });
449
+
450
+ this.action('ArrowRight', false, ( ln, cursor, e ) => {
451
+
452
+ if(e.metaKey) { // Apple devices (Command)
453
+ e.preventDefault();
454
+ this.actions[ 'End' ].callback( ln, cursor );
455
+ } else if(e.ctrlKey) {
456
+ // Get next word
457
+ const [word, from, to] = this.getWordAtPos( cursor );
458
+ var diff = cursor.position - from;
459
+ var substr = word.substr(diff);
460
+ // Selections...
461
+ if( e.shiftKey ) if(!this.selection) this.startSelection(cursor);
462
+ this.cursorToString(cursor, substr);
463
+ if( e.shiftKey ) this.processSelection();
464
+ } else {
465
+ var letter = this.getCharAtPos( cursor );
466
+ if(letter) {
467
+ if( e.shiftKey ) {
468
+ if(!this.selection) this.startSelection(cursor);
469
+ var keep_range = false;
470
+ if( cursor.position == this.selection.fromX ) {
471
+ if( (cursor.position + 1) == this.selection.toX && this.selection.sameLine() ) {
472
+ this.cursorToRight( letter, cursor );
473
+ this.endSelection();
474
+ return;
475
+ } else if( cursor.position < this.selection.toX ) {
476
+ this.selection.fromX++;
477
+ keep_range = true;
478
+ } else this.selection.toX++;
479
+ }
480
+ this.cursorToRight( letter, cursor );
481
+ this.processSelection(null, keep_range);
482
+ }else{
483
+ if(!this.selection) this.cursorToRight( letter, cursor );
484
+ else
485
+ {
486
+ this.selection.invertIfNecessary();
487
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
488
+ this.cursorToLine(cursor, this.selection.toY);
489
+ this.cursorToPosition(cursor, this.selection.toX);
490
+ this.endSelection();
491
+ }
492
+ }
493
+ }
494
+ else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
495
+
496
+ this.lineDown( cursor );
497
+ e.cancelShift = true;
498
+ this.actions['Home'].callback(cursor.line, cursor, e);
499
+
500
+ if( e.shiftKey ) {
501
+ if(!this.selection) this.startSelection(cursor);
502
+ this.selection.toX = cursor.position;
503
+ this.selection.toY++;
504
+ this.processSelection(null, true);
505
+ }
506
+ }
507
+ }
508
+ });
509
+
510
+ // Default code tab
511
+
512
+ this.openedTabs = { };
513
+
514
+ if( options.allow_add_scripts ?? true )
515
+ this.addTab("+", false, "New File");
516
+
517
+ this.addTab(options.name || "untitled", true, options.title);
518
+
519
+ // Create inspector panel
520
+ let panel = this._create_panel_info();
521
+ if( panel ) area.attach( panel );
522
+ }
523
+
524
+ static getInstances()
525
+ {
526
+ return CodeEditor.__instances;
527
+ }
528
+
529
+ getText( min ) {
530
+ return this.code.lines.join(min ? ' ' : '\n');
531
+ }
532
+
533
+ setText(text) {
534
+ const new_lines = text.split('\n');
535
+
536
+ if (new_lines.length < 1) { return; }
537
+
538
+ let num_lines = new_lines.length;
539
+ console.assert(num_lines > 0);
540
+ const first_line = new_lines.shift();
541
+ num_lines--;
542
+
543
+ let cursor = this.cursors.children[0];
544
+ let lidx = cursor.line;
545
+ const remaining = this.code.lines[lidx].slice(cursor.position);
546
+
547
+ // Add first line
548
+ this.code.lines[lidx] = [
549
+ this.code.lines[lidx].slice(0, cursor.position),
550
+ first_line
551
+ ].join('');
552
+
553
+ this.cursorToPosition(cursor, (cursor.position + first_line.length));
554
+
555
+ // Enter next lines...
556
+ let _text = null;
557
+
558
+ for (var i = 0; i < new_lines.length; ++i) {
559
+ _text = new_lines[i];
560
+ this.cursorToLine(cursor, cursor.line++, true);
561
+ // Add remaining...
562
+ if (i == (new_lines.length - 1))
563
+ _text += remaining;
564
+ this.code.lines.splice(1 + lidx + i, 0, _text);
565
+ }
566
+
567
+ if (_text) this.cursorToPosition(cursor, _text.length);
568
+ this.cursorToLine(cursor, cursor.line + num_lines);
569
+ this.processLines(lidx);
570
+ }
571
+
572
+ loadFile( file ) {
573
+
574
+ const inner_add_tab = ( text, name, title ) => {
575
+ this.addTab(name, true, title);
576
+ text = text.replaceAll('\r', '');
577
+ this.code.lines = text.split('\n');
578
+ this.processLines();
579
+ this._refresh_code_info();
580
+ };
581
+
582
+ if(file.constructor == String)
583
+ {
584
+ let filename = file;
585
+ LX.request({ url: filename, success: text => {
586
+
587
+ const name = filename.substring(filename.lastIndexOf('/') + 1);
588
+ this._change_language_from_extension( LX.getExtension(name) );
589
+ inner_add_tab( text, name, filename );
590
+ } });
591
+ }
592
+ else // File Blob
593
+ {
594
+ const fr = new FileReader();
595
+ fr.readAsText( file );
596
+ fr.onload = e => {
597
+ this._change_language_from_extension( LX.getExtension(file.name) );
598
+ const text = e.currentTarget.result;
599
+ inner_add_tab( text, file.name );
600
+ };
601
+ }
602
+
603
+ }
604
+
605
+ _addUndoStep( cursor ) {
606
+
607
+ var cursor = cursor ?? this.cursors.children[0];
608
+
609
+ this.code.undoSteps.push( {
610
+ lines: LX.deepCopy(this.code.lines),
611
+ cursor: this.saveCursor(cursor),
612
+ line: cursor.line
613
+ } );
614
+ }
615
+
616
+ _change_language( lang ) {
617
+ this.highlight = lang;
618
+ this._refresh_code_info();
619
+ this.processLines();
620
+ }
621
+
622
+ _change_language_from_extension( ext ) {
623
+
624
+ switch(ext.toLowerCase())
625
+ {
626
+ case 'js': return this._change_language('JavaScript');
627
+ case 'glsl': return this._change_language('GLSL');
628
+ case 'css': return this._change_language('CSS');
629
+ case 'json': return this._change_language('JSON');
630
+ case 'xml': return this._change_language('XML');
631
+ case 'wgsl': return this._change_language('WGSL');
632
+ case 'txt':
633
+ default:
634
+ this._change_language('Plain Text');
635
+ }
636
+ }
637
+
638
+ _create_panel_info() {
639
+
640
+ if( !this.skip_info )
641
+ {
642
+ let panel = new LX.Panel({ className: "lexcodetabinfo", width: "calc(100%)", height: "auto" });
643
+ panel.ln = 0;
644
+ panel.col = 0;
645
+
646
+ this._refresh_code_info = (ln = panel.ln, col = panel.col) => {
647
+ panel.ln = ln;
648
+ panel.col = col;
649
+ panel.clear();
650
+ panel.sameLine();
651
+ panel.addLabel(this.code.title, { float: 'right' });
652
+ panel.addLabel("Ln " + ln, { width: "64px" });
653
+ panel.addLabel("Col " + col, { width: "64px" });
654
+ panel.addButton("<b>{ }</b>", this.highlight, (value, event) => {
655
+ LX.addContextMenu( "Language", event, m => {
656
+ for( const lang of this.languages )
657
+ m.add( lang, this._change_language.bind(this) );
658
+ });
659
+ }, { width: "25%", nameWidth: "15%" });
660
+ panel.endLine();
661
+ };
662
+
663
+ this._refresh_code_info();
664
+
665
+ return panel;
666
+ }
667
+ else
668
+ {
669
+ this._refresh_code_info = () => {};
670
+
671
+ doAsync( () => {
672
+
673
+ // Change css a little bit...
674
+ this.gutter.style.height = "calc(100% - 38px)";
675
+ this.root.querySelectorAll('.code').forEach( e => e.style.height = "calc(100% - 6px)" );
676
+ this.root.querySelector('.lexareatabscontent').style.height = "calc(100% - 23px)";
677
+
678
+ }, 100);
679
+ }
680
+ }
681
+
682
+ _onNewTab( e ) {
683
+
684
+ this.processFocus(false);
685
+
686
+ LX.addContextMenu( null, e, m => {
687
+ m.add( "Create", this.addTab.bind(this, "unnamed.js", true) );
688
+ m.add( "Load", this.loadTab.bind(this, "unnamed.js", true) );
689
+ });
690
+ }
691
+
692
+ addTab(name, selected, title) {
693
+
694
+ if(this.openedTabs[name])
695
+ {
696
+ this.tabs.select( this.code.tabName );
697
+ return;
698
+ }
699
+
700
+ // Create code content
701
+ let code = document.createElement('div');
702
+ code.className = 'code';
703
+ code.lines = [""];
704
+ code.cursorState = {};
705
+ code.undoSteps = [];
706
+ code.tabName = name;
707
+ code.title = title ?? name;
708
+
709
+ code.addEventListener('dragenter', function(e) {
710
+ e.preventDefault();
711
+ this.classList.add('dragging');
712
+ });
713
+ code.addEventListener('dragleave', function(e) {
714
+ e.preventDefault();
715
+ this.classList.remove('dragging');
716
+ });
717
+ code.addEventListener('drop', (e) => {
718
+ e.preventDefault();
719
+ code.classList.remove('dragging');
720
+ for( let i = 0; i < e.dataTransfer.files.length; ++i )
721
+ this.loadFile( e.dataTransfer.files[i] );
722
+ });
723
+
724
+ code.addEventListener('scroll', (e) => {
725
+ this.gutter.scrollTop = code.scrollTop;
726
+ this.gutter.scrollLeft = code.scrollLeft;
727
+
728
+ // Update cursor
729
+ var cursor = this.cursors.children[0];
730
+ cursor.style.top = (cursor._top - code.scrollTop) + "px";
731
+ cursor.style.left = "calc( " + (cursor._left - code.scrollLeft) + "px + " + this.xPadding + ")";
732
+
733
+ // Update selection
734
+ for( let s of this.selections.childNodes ) {
735
+ s.style.top = (s._top - code.scrollTop) + "px";
736
+ s.style.left = "calc( " + (s._left - code.scrollLeft) + "px + " + this.xPadding + ")";
737
+ }
738
+ });
739
+
740
+ this.openedTabs[name] = code;
741
+
742
+ this.tabs.add(name, code, { 'selected': selected, 'fixed': (name === '+') , 'title': code.title, 'onSelect': (e, tabname) => {
743
+
744
+ if(tabname == '+')
745
+ {
746
+ this._onNewTab( e );
747
+ return;
748
+ }
749
+
750
+ var cursor = cursor ?? this.cursors.children[0];
751
+ this.saveCursor(cursor, this.code.cursorState);
752
+ this.code = this.openedTabs[tabname];
753
+ this.restoreCursor(cursor, this.code.cursorState);
754
+ this.endSelection();
755
+ this._change_language_from_extension( LX.getExtension(tabname) );
756
+ this._refresh_code_info(cursor.line + 1, cursor.position);
757
+
758
+ // Restore scroll
759
+ this.gutter.scrollTop = this.code.scrollTop;
760
+ this.gutter.scrollLeft = this.code.scrollLeft;
761
+ }});
762
+
763
+ this.endSelection();
764
+
765
+ if(selected){
766
+ this.code = code;
767
+ this.resetCursorPos(CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP);
768
+ this.processLines();
769
+ doAsync( () => this._refresh_code_info(0, 0), 50 );
770
+ }
771
+ }
772
+
773
+ loadTab() {
774
+ const input = document.createElement('input');
775
+ input.type = 'file';
776
+ document.body.appendChild(input);
777
+ input.click();
778
+ input.addEventListener('change', (e) => {
779
+ if (e.target.files[0]) {
780
+ this.loadFile( e.target.files[0] );
781
+ }
782
+ input.remove();
783
+ });
784
+ }
785
+
786
+ processFocus( active ) {
787
+
788
+ if( active )
789
+ this.restartBlink();
790
+ else {
791
+ clearInterval( this.blinker );
792
+ this.cursors.classList.remove('show');
793
+ }
794
+ }
795
+
796
+ processMouse(e) {
797
+
798
+ if( !e.target.classList.contains('code') ) return;
799
+ if( !this.code ) return;
800
+
801
+ var cursor = this.cursors.children[0];
802
+
803
+ // Discard out of lines click...
804
+ if( e.type != 'contextmenu' )
805
+ {
806
+ var code_rect = this.code.getBoundingClientRect();
807
+ var mouse_pos = [(e.clientX - code_rect.x) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
808
+ var ln = (mouse_pos[1] / this.lineHeight)|0;
809
+ if(this.code.lines[ln] == undefined) return;
810
+ }
811
+
812
+ if( e.type == 'mousedown' )
813
+ {
814
+ if( mouse_pos[0] > this.code.scrollWidth || mouse_pos[1] > this.code.scrollHeight )
815
+ return; // Scrollbar click
816
+ this.lastMouseDown = LX.getTime();
817
+ this.state.selectingText = true;
818
+ this.endSelection();
819
+ return;
820
+ }
821
+
822
+ else if( e.type == 'mouseup' )
823
+ {
824
+ if( (LX.getTime() - this.lastMouseDown) < 300 ) {
825
+ this.state.selectingText = false;
826
+ this.processClick(e);
827
+ this.endSelection();
828
+ }
829
+
830
+ if(this.selection) this.selection.invertIfNecessary();
831
+
832
+ this.state.selectingText = false;
833
+ }
834
+
835
+ else if( e.type == 'mousemove' )
836
+ {
837
+ if( this.state.selectingText )
838
+ this.processSelection(e);
839
+ }
840
+
841
+ else if ( e.type == 'click' ) // trip
842
+ {
843
+ switch( e.detail )
844
+ {
845
+ case LX.MOUSE_DOUBLE_CLICK:
846
+ const [word, from, to] = this.getWordAtPos( cursor );
847
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
848
+ this.cursorToPosition( cursor, from );
849
+ this.startSelection( cursor );
850
+ this.selection.selectInline(from, cursor.line, this.measureString(word));
851
+ this.cursorToString( cursor, word ); // Go to the end of the word
852
+ break;
853
+ // Select entire line
854
+ case LX.MOUSE_TRIPLE_CLICK:
855
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT );
856
+ e._shiftKey = true;
857
+ this.actions['End'].callback(cursor.line, cursor, e);
858
+ break;
859
+ }
860
+ }
861
+
862
+ else if ( e.type == 'contextmenu' ) {
863
+ e.preventDefault()
864
+ LX.addContextMenu( "Format code", e, m => {
865
+ m.add( "JSON", () => {
866
+ let json = this.toJSONFormat(this.getText());
867
+ this.code.lines = json.split("\n");
868
+ this.processLines();
869
+ } );
870
+ });
871
+ }
872
+ }
873
+
874
+ processClick(e, skip_refresh = false) {
875
+
876
+ var code_rect = this.code.getBoundingClientRect();
877
+ var position = [(e.clientX - code_rect.x) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
878
+ var ln = (position[1] / this.lineHeight)|0;
879
+
880
+ if(this.code.lines[ln] == undefined) return;
881
+
882
+ var cursor = this.cursors.children[0];
883
+ cursor.line = ln;
884
+
885
+ this.cursorToLine(cursor, ln, true);
886
+
887
+ var ch = (position[0] / this.charWidth)|0;
888
+ var string = this.code.lines[ln].slice(0, ch);
889
+ // this.cursorToString(cursor, string);
890
+ this.cursorToPosition(cursor, string.length);
891
+
892
+ if(!skip_refresh)
893
+ this._refresh_code_info( ln + 1, cursor.position );
894
+ }
895
+
896
+ processSelection( e, keep_range ) {
897
+
898
+ var cursor = this.cursors.children[0];
899
+
900
+ if(e) this.processClick(e, true);
901
+ if( !this.selection )
902
+ this.startSelection(cursor);
903
+
904
+ // Update selection
905
+ if(!keep_range)
906
+ {
907
+ this.selection.toX = cursor.position;
908
+ this.selection.toY = cursor.line;
909
+ }
910
+
911
+ this.selection.chars = 0;
912
+
913
+ const fromX = this.selection.fromX,
914
+ fromY = this.selection.fromY,
915
+ toX = this.selection.toX,
916
+ toY = this.selection.toY;
917
+ const deltaY = toY - fromY;
918
+
919
+ // Selection goes down...
920
+ if( deltaY >= 0 )
921
+ {
922
+ while( deltaY < (this.selections.childElementCount - 1) )
923
+ this.selections.lastChild.remove();
924
+
925
+ for(let i = fromY; i <= toY; i++){
926
+
927
+ const sId = i - fromY;
928
+
929
+ // Make sure that the line selection is generated...
930
+ let domEl = this.selections.childNodes[sId];
931
+ if(!domEl)
932
+ {
933
+ domEl = document.createElement('div');
934
+ domEl.className = "lexcodeselection";
935
+ this.selections.appendChild( domEl );
936
+ }
937
+
938
+ // Compute new width and selection margins
939
+ let string;
940
+
941
+ if(sId == 0) // First line 2 cases (single line, multiline)
942
+ {
943
+ const reverse = fromX > toX;
944
+ if(deltaY == 0) string = !reverse ? this.code.lines[i].substring(fromX, toX) : this.code.lines[i].substring(toX, fromX);
945
+ else string = this.code.lines[i].substr(fromX);
946
+ const pixels = ((reverse && deltaY == 0 ? toX : fromX) * this.charWidth) - this.getScrollLeft();
947
+ domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
948
+ }
949
+ else
950
+ {
951
+ string = (i == toY) ? this.code.lines[i].substring(0, toX) : this.code.lines[i]; // Last line, any multiple line...
952
+ const pixels = -this.getScrollLeft();
953
+ domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
954
+ }
955
+
956
+ const stringWidth = this.measureString(string);
957
+ domEl.style.width = (stringWidth || 8) + "px";
958
+ domEl._top = 4 + i * this.lineHeight;
959
+ domEl.style.top = (domEl._top - this.getScrollTop()) + "px";
960
+ this.selection.chars += stringWidth / this.charWidth;
961
+ }
962
+ }
963
+ else // Selection goes up...
964
+ {
965
+ while( Math.abs(deltaY) < (this.selections.childElementCount - 1) )
966
+ this.selections.firstChild.remove();
967
+
968
+ for(let i = toY; i <= fromY; i++){
969
+
970
+ const sId = i - toY;
971
+
972
+ // Make sure that the line selection is generated...
973
+ let domEl = this.selections.childNodes[sId];
974
+ if(!domEl)
975
+ {
976
+ domEl = document.createElement('div');
977
+ domEl.className = "lexcodeselection";
978
+ this.selections.appendChild( domEl );
979
+ }
980
+
981
+ // Compute new width and selection margins
982
+ let string;
983
+
984
+ if(sId == 0)
985
+ {
986
+ string = this.code.lines[i].substr(toX);
987
+ const pixels = (toX * this.charWidth) - this.getScrollLeft();
988
+ domEl.style.left = "calc(" + pixels + "px + " + this.xPadding + ")";
989
+ }
990
+ else
991
+ {
992
+ string = (i == fromY) ? this.code.lines[i].substring(0, fromX) : this.code.lines[i]; // Last line, any multiple line...
993
+ domEl.style.left = "calc(" + (-this.getScrollLeft()) + "px + " + this.xPadding + ")";
994
+ }
995
+
996
+ const stringWidth = this.measureString(string);
997
+ domEl.style.width = (stringWidth || 8) + "px";
998
+ domEl._top = 4 + i * this.lineHeight;
999
+ domEl.style.top = (domEl._top - this.getScrollTop()) + "px";
1000
+ this.selection.chars += stringWidth / this.charWidth;
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ async processKey(e) {
1006
+
1007
+ if( !this.code )
1008
+ return;
1009
+
1010
+ var key = e.key ?? e.detail.key;
1011
+
1012
+ const skip_undo = e.detail.skip_undo ?? false;
1013
+
1014
+ // keys with length > 1 are probably special keys
1015
+ if( key.length > 1 && this.specialKeys.indexOf(key) == -1 )
1016
+ return;
1017
+
1018
+ let cursor = this.cursors.children[0];
1019
+ let lidx = cursor.line;
1020
+ this.code.lines[lidx] = this.code.lines[lidx] ?? "";
1021
+
1022
+ // Check combinations
1023
+
1024
+ if( e.ctrlKey || e.metaKey )
1025
+ {
1026
+ switch( key.toLowerCase() ) {
1027
+ case 'a': // select all
1028
+ e.preventDefault();
1029
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP );
1030
+ this.startSelection(cursor);
1031
+ const nlines = this.code.lines.length - 1;
1032
+ this.selection.toX = this.code.lines[nlines].length;
1033
+ this.selection.toY = nlines;
1034
+ this.processSelection(null, true);
1035
+ this.cursorToPosition(cursor, this.selection.toX);
1036
+ this.cursorToLine(cursor, this.selection.toY);
1037
+ break;
1038
+ case 'c': // copy
1039
+ let text_to_copy = "";
1040
+ if( !this.selection ) {
1041
+ text_to_copy = "\n" + this.code.lines[cursor.line];
1042
+ }
1043
+ else {
1044
+ const separator = "_NEWLINE_";
1045
+ let code = this.code.lines.join(separator);
1046
+
1047
+ // Get linear start index
1048
+ let index = 0;
1049
+
1050
+ for(let i = 0; i <= this.selection.fromY; i++)
1051
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1052
+
1053
+ index += this.selection.fromY * separator.length;
1054
+ const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
1055
+ const text = code.substr(index, num_chars);
1056
+ const lines = text.split(separator);
1057
+ text_to_copy = lines.join('\n');
1058
+ }
1059
+ navigator.clipboard.writeText(text_to_copy).then(() => console.log("Successfully copied"), (err) => console.error("Error"));
1060
+ return;
1061
+ case 'd': // duplicate line
1062
+ e.preventDefault();
1063
+ this.code.lines.splice(lidx, 0, this.code.lines[lidx]);
1064
+ this.lineDown( cursor );
1065
+ this.processLines(lidx);
1066
+ return;
1067
+ case 's': // save
1068
+ e.preventDefault();
1069
+ this.onsave( this.getText() );
1070
+ return;
1071
+ case 'v': // paste
1072
+
1073
+ if( this.selection ) {
1074
+ this.deleteSelection(cursor);
1075
+ lidx = cursor.line;
1076
+ }
1077
+
1078
+ this.endSelection();
1079
+
1080
+ let text = await navigator.clipboard.readText();
1081
+ const new_lines = text.split('\n');
1082
+
1083
+ // Pasting Multiline...
1084
+ if(new_lines.length != 1)
1085
+ {
1086
+ let num_lines = new_lines.length;
1087
+ console.assert(num_lines > 0);
1088
+ const first_line = new_lines.shift();
1089
+ num_lines--;
1090
+
1091
+ const remaining = this.code.lines[lidx].slice(cursor.position);
1092
+
1093
+ // Add first line
1094
+ this.code.lines[lidx] = [
1095
+ this.code.lines[lidx].slice(0, cursor.position),
1096
+ first_line
1097
+ ].join('');
1098
+
1099
+ this.cursorToPosition(cursor, (cursor.position + first_line.length));
1100
+
1101
+ // Enter next lines...
1102
+
1103
+ let _text = null;
1104
+
1105
+ for( var i = 0; i < new_lines.length; ++i ) {
1106
+ _text = new_lines[i];
1107
+ this.cursorToLine(cursor, cursor.line++, true);
1108
+ // Add remaining...
1109
+ if( i == (new_lines.length - 1) )
1110
+ _text += remaining;
1111
+ this.code.lines.splice( 1 + lidx + i, 0, _text);
1112
+ }
1113
+
1114
+ if(_text) this.cursorToPosition(cursor, _text.length);
1115
+ this.cursorToLine(cursor, cursor.line + num_lines);
1116
+ this.processLines(lidx);
1117
+ }
1118
+ // Pasting one line...
1119
+ else
1120
+ {
1121
+ this.code.lines[lidx] = [
1122
+ this.code.lines[lidx].slice(0, cursor.position),
1123
+ new_lines[0],
1124
+ this.code.lines[lidx].slice(cursor.position)
1125
+ ].join('');
1126
+
1127
+ this.cursorToPosition(cursor, (cursor.position + new_lines[0].length));
1128
+ this.processLine(lidx);
1129
+ }
1130
+ return;
1131
+ case 'x': // cut line
1132
+ const to_copy = this.code.lines.splice(lidx, 1)[0];
1133
+ this.processLines(lidx);
1134
+ navigator.clipboard.writeText(to_copy + '\n');
1135
+ return;
1136
+ case 'z': // undo
1137
+ if(!this.code.undoSteps.length)
1138
+ return;
1139
+ const step = this.code.undoSteps.pop();
1140
+ this.code.lines = step.lines;
1141
+ cursor.line = step.line;
1142
+ this.restoreCursor( cursor, step.cursor );
1143
+ this.processLines();
1144
+ return;
1145
+
1146
+
1147
+ }
1148
+ }
1149
+
1150
+ else if( e.altKey )
1151
+ {
1152
+ switch( key ) {
1153
+ case 'ArrowUp':
1154
+ if(this.code.lines[ lidx - 1 ] == undefined)
1155
+ return;
1156
+ swapArrayElements(this.code.lines, lidx - 1, lidx);
1157
+ this.lineUp();
1158
+ this.processLine( lidx - 1 );
1159
+ this.processLine( lidx );
1160
+ return;
1161
+ case 'ArrowDown':
1162
+ if(this.code.lines[ lidx + 1 ] == undefined)
1163
+ return;
1164
+ swapArrayElements(this.code.lines, lidx, lidx + 1);
1165
+ this.lineDown();
1166
+ this.processLine( lidx );
1167
+ this.processLine( lidx + 1 );
1168
+ return;
1169
+ }
1170
+ }
1171
+
1172
+ // Apply binded actions...
1173
+
1174
+ for( const actKey in this.actions ) {
1175
+ if( key != actKey ) continue;
1176
+ e.preventDefault();
1177
+
1178
+ if(this.actions[ key ].deleteSelection && this.selection)
1179
+ this.actions['Backspace'].callback(lidx, cursor, e);
1180
+
1181
+ return this.actions[ key ].callback( lidx, cursor, e );
1182
+ }
1183
+
1184
+ // From now on, don't allow ctrl, shift or meta (mac) combinations
1185
+ if( (e.ctrlKey || e.metaKey) )
1186
+ return;
1187
+
1188
+ // Add undo steps
1189
+
1190
+ const d = new Date();
1191
+ const current = d.getTime();
1192
+
1193
+ if( !skip_undo )
1194
+ {
1195
+ if( !this._lastTime ) {
1196
+ this._lastTime = current;
1197
+ this._addUndoStep( cursor );
1198
+ } else {
1199
+ if( (current - this._lastTime) > 3000 && this.code.lines.length){
1200
+ this._lastTime = null;
1201
+ this._addUndoStep( cursor );
1202
+ }else{
1203
+ // If time not enough, reset timer
1204
+ this._lastTime = current;
1205
+ }
1206
+ }
1207
+ }
1208
+
1209
+ // Some custom cases for word enclosing (), {}, "", '', ...
1210
+
1211
+ const enclosableKeys = ["\"", "'", "(", "{"];
1212
+ if( enclosableKeys.indexOf( key ) > -1 )
1213
+ {
1214
+ if( this.encloseSelectedWordWithKey(key, lidx, cursor) )
1215
+ return;
1216
+ }
1217
+
1218
+ // Until this point, if there was a selection, we need
1219
+ // to delete the content..
1220
+
1221
+ if( this.selection )
1222
+ {
1223
+ this.actions['Backspace'].callback(lidx, cursor, e);
1224
+ }
1225
+
1226
+ // Append key
1227
+
1228
+ this.code.lines[lidx] = [
1229
+ this.code.lines[lidx].slice(0, cursor.position),
1230
+ key,
1231
+ this.code.lines[lidx].slice(cursor.position)
1232
+ ].join('');
1233
+
1234
+ this.cursorToRight( key );
1235
+
1236
+ // Other special char cases...
1237
+
1238
+ if( key == '{' )
1239
+ {
1240
+ this.root.dispatchEvent(new KeyboardEvent('keydown', {'key': '}'}));
1241
+ this.cursorToLeft( key, cursor );
1242
+ return; // It will be processed with the above event
1243
+ }
1244
+
1245
+ // Update only the current line, since it's only an appended key
1246
+ this.processLine( lidx );
1247
+ }
1248
+
1249
+ action( key, deleteSelection, fn ) {
1250
+
1251
+ this.actions[ key ] = {
1252
+ "callback": fn,
1253
+ "deleteSelection": deleteSelection
1254
+ };
1255
+ }
1256
+
1257
+ processLines( from ) {
1258
+
1259
+ if( !from )
1260
+ {
1261
+ this.gutter.innerHTML = "";
1262
+ this.code.innerHTML = "";
1263
+ }
1264
+
1265
+ for( let i = from ?? 0; i < this.code.lines.length; ++i )
1266
+ {
1267
+ this.processLine( i );
1268
+ }
1269
+
1270
+ // Clean up...
1271
+ if( from )
1272
+ {
1273
+ while( this.code.lines.length != this.gutter.children.length )
1274
+ this.gutter.lastChild.remove();
1275
+ while( this.code.lines.length != this.code.children.length )
1276
+ this.code.lastChild.remove();
1277
+ }
1278
+
1279
+ }
1280
+
1281
+ processLine( linenum ) {
1282
+
1283
+ delete this._building_string; // multi-line strings not supported by now
1284
+
1285
+ // It's allowed to process only 1 line to optimize
1286
+ let linestring = this.code.lines[ linenum ];
1287
+ var _lines = this.code.querySelectorAll('pre');
1288
+ var pre = null, single_update = false;
1289
+ if( _lines[linenum] ) {
1290
+ pre = _lines[linenum];
1291
+ single_update = true;
1292
+ }
1293
+
1294
+ if(!pre)
1295
+ {
1296
+ var pre = document.createElement('pre');
1297
+ pre.dataset['linenum'] = linenum;
1298
+ this.code.appendChild(pre);
1299
+ }
1300
+ else
1301
+ {
1302
+ pre.children[0].remove(); // Remove token list
1303
+ }
1304
+
1305
+ var linespan = document.createElement('span');
1306
+ pre.appendChild(linespan);
1307
+
1308
+ // Check if line comment
1309
+ const is_comment = linestring.split('//');
1310
+ linestring = ( is_comment.length > 1 ) ? is_comment[0] : linestring;
1311
+
1312
+ const tokens = linestring.split(' ').join('¬ ¬').split('¬'); // trick to split without losing spaces
1313
+ const to_process = []; // store in a temp array so we know prev and next tokens...
1314
+
1315
+ for( let t of tokens )
1316
+ {
1317
+ let iter = t.matchAll(/[\[\](){}<>.,;:"']/g);
1318
+ let subtokens = iter.next();
1319
+ if( subtokens.value )
1320
+ {
1321
+ let idx = 0;
1322
+ while( subtokens.value != undefined )
1323
+ {
1324
+ const _pt = t.substring(idx, subtokens.value.index);
1325
+ to_process.push( _pt );
1326
+ to_process.push( subtokens.value[0] );
1327
+ idx = subtokens.value.index + 1;
1328
+ subtokens = iter.next();
1329
+ if(!subtokens.value) {
1330
+ const _at = t.substring(idx);
1331
+ to_process.push( _at );
1332
+ }
1333
+ }
1334
+ }
1335
+ else
1336
+ to_process.push( t );
1337
+ }
1338
+
1339
+ if( is_comment.length > 1 )
1340
+ to_process.push( "//" + is_comment[1] );
1341
+
1342
+ // Process all tokens
1343
+ for( var i = 0; i < to_process.length; ++i )
1344
+ {
1345
+ let it = i - 1;
1346
+ let prev = to_process[it];
1347
+ while( prev == '' || prev == ' ' ) {
1348
+ it--;
1349
+ prev = to_process[it];
1350
+ }
1351
+
1352
+ it = i + 1;
1353
+ let next = to_process[it];
1354
+ while( next == '' || next == ' ' || next == '"') {
1355
+ it++;
1356
+ next = to_process[it];
1357
+ }
1358
+
1359
+ let token = to_process[i];
1360
+ if( token.substr(0, 2) == '/*' )
1361
+ this._building_block_comment = true;
1362
+ if( token.substr(token.length - 2) == '*/' )
1363
+ delete this._building_block_comment;
1364
+
1365
+ this.processToken(token, linespan, prev, next);
1366
+ }
1367
+
1368
+ // add line gutter
1369
+ if(!single_update)
1370
+ {
1371
+ var linenumspan = document.createElement('span');
1372
+ linenumspan.innerHTML = (linenum + 1);
1373
+ this.gutter.appendChild(linenumspan);
1374
+ }
1375
+ }
1376
+
1377
+ processToken(token, linespan, prev, next) {
1378
+
1379
+ let sString = false;
1380
+ let highlight = this.highlight.replace(/\s/g, '').toLowerCase();
1381
+
1382
+ if(token == '"' || token == "'")
1383
+ {
1384
+ sString = (this._building_string == token); // stop string if i was building it
1385
+ this._building_string = this._building_string ? this._building_string : token;
1386
+ }
1387
+
1388
+ if(token == ' ')
1389
+ {
1390
+ linespan.innerHTML += token;
1391
+ }
1392
+ else
1393
+ {
1394
+ var span = document.createElement('span');
1395
+ span.innerHTML = token;
1396
+
1397
+ if( this._building_block_comment )
1398
+ span.classList.add("cm-com");
1399
+
1400
+ else if( this._building_string )
1401
+ span.classList.add("cm-str");
1402
+
1403
+ else if( this.keywords[this.highlight] && this.keywords[this.highlight].indexOf(token) > -1 )
1404
+ span.classList.add("cm-kwd");
1405
+
1406
+ else if( this.builtin[this.highlight] && this.builtin[this.highlight].indexOf(token) > -1 )
1407
+ span.classList.add("cm-bln");
1408
+
1409
+ else if( this.literals[this.highlight] && this.literals[this.highlight].indexOf(token) > -1 )
1410
+ span.classList.add("cm-lit");
1411
+
1412
+ else if( this.symbols[this.highlight] && this.symbols[this.highlight].indexOf(token) > -1 )
1413
+ span.classList.add("cm-sym");
1414
+
1415
+ else if( token.substr(0, 2) == '//' )
1416
+ span.classList.add("cm-com");
1417
+
1418
+ else if( token.substr(0, 2) == '/*' )
1419
+ span.classList.add("cm-com");
1420
+
1421
+ else if( token.substr(token.length - 2) == '*/' )
1422
+ span.classList.add("cm-com");
1423
+
1424
+ else if( this.isNumber(token) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
1425
+ span.classList.add("cm-dec");
1426
+
1427
+ else if( this.isCSSClass(token, prev, next) )
1428
+ span.classList.add("cm-kwd");
1429
+
1430
+ else if ( this.isType(token, prev, next) )
1431
+ span.classList.add("cm-typ");
1432
+
1433
+ else if ( token[0] != '@' && next == '(' )
1434
+ span.classList.add("cm-mtd");
1435
+
1436
+ else if ( highlight == 'css' && prev == ':' && (next == ';' || next == '!important') ) // CSS value
1437
+ span.classList.add("cm-str");
1438
+
1439
+ else if ( highlight == 'css' && prev == undefined && next == ':' ) // CSS attribute
1440
+ span.classList.add("cm-typ");
1441
+
1442
+
1443
+ span.classList.add(highlight);
1444
+ linespan.appendChild(span);
1445
+ }
1446
+
1447
+ if(sString) delete this._building_string;
1448
+ }
1449
+
1450
+ isCSSClass( token, prev, next ) {
1451
+ return this.highlight == 'CSS' && prev == '.';
1452
+ }
1453
+
1454
+ isNumber( token ) {
1455
+ return token.length && !Number.isNaN(+token);
1456
+ }
1457
+
1458
+ isType( token, prev, next ) {
1459
+
1460
+ if( this.highlight == 'JavaScript' )
1461
+ {
1462
+ return (prev == 'class' && next == '{') || (prev == 'new' && next == '(');
1463
+ }
1464
+ else if ( this.highlight == 'WGSL' )
1465
+ {
1466
+ const is_kwd = (this.keywords[this.highlight] && this.keywords[this.highlight].indexOf(token) == -1);
1467
+ return (prev == 'struct' && next == '{') ||
1468
+ ( is_kwd &&
1469
+ ( prev == ':' && next == ')' || prev == ':' && next == ',' || prev == '>' && next == '{'
1470
+ || prev == '<' && next == ',' || prev == '<' && next == '>' || prev == '>' && !next ));
1471
+ }
1472
+ }
1473
+
1474
+ encloseSelectedWordWithKey( key, lidx, cursor ) {
1475
+
1476
+ if( !this.selection || (this.selection.fromY != this.selection.toY) )
1477
+ return false;
1478
+
1479
+ const _lastLeft = cursor._left;
1480
+
1481
+ // Insert first..
1482
+ this.code.lines[lidx] = [
1483
+ this.code.lines[lidx].slice(0, this.selection.fromX),
1484
+ key,
1485
+ this.code.lines[lidx].slice(this.selection.fromX)
1486
+ ].join('');
1487
+
1488
+ // Go to the end of the word
1489
+ this.cursorToString(cursor,
1490
+ this.code.lines[lidx].slice(this.selection.fromX, this.selection.toX + 1));
1491
+
1492
+ // Change next key?
1493
+ switch(key)
1494
+ {
1495
+ case "'":
1496
+ case "\"":
1497
+ break;
1498
+ case "(": key = ")"; break;
1499
+ case "{": key = "}"; break;
1500
+ }
1501
+
1502
+ // Insert the other
1503
+ this.code.lines[lidx] = [
1504
+ this.code.lines[lidx].slice(0, cursor.position),
1505
+ key,
1506
+ this.code.lines[lidx].slice(cursor.position)
1507
+ ].join('');
1508
+
1509
+ // Recompute and reposition current selection
1510
+
1511
+ this.selection.fromX++;
1512
+ this.selection.toX++;
1513
+
1514
+ this.processSelection();
1515
+ this.processLine( lidx );
1516
+
1517
+ // Stop propagation
1518
+ return true;
1519
+ }
1520
+
1521
+ lineUp(cursor, resetLeft) {
1522
+
1523
+ cursor = cursor ?? this.cursors.children[0];
1524
+ cursor.line--;
1525
+ cursor.line = Math.max(0, cursor.line);
1526
+ this.cursorToTop(cursor, resetLeft);
1527
+ }
1528
+
1529
+ lineDown(cursor, resetLeft) {
1530
+
1531
+ cursor = cursor ?? this.cursors.children[0];
1532
+ cursor.line++;
1533
+ this.cursorToBottom(cursor, resetLeft);
1534
+ }
1535
+
1536
+ restartBlink() {
1537
+
1538
+ if( !this.code ) return;
1539
+
1540
+ clearInterval(this.blinker);
1541
+ this.cursors.classList.add('show');
1542
+
1543
+ if (this.cursorBlinkRate > 0)
1544
+ this.blinker = setInterval(() => {
1545
+ this.cursors.classList.toggle('show');
1546
+ }, this.cursorBlinkRate);
1547
+ else if (this.cursorBlinkRate < 0)
1548
+ this.cursors.classList.remove('show');
1549
+ }
1550
+
1551
+ startSelection( cursor ) {
1552
+
1553
+ // Clear other selections...
1554
+ this.selections.innerHTML = "";
1555
+
1556
+ // Show elements
1557
+ this.selections.classList.add('show');
1558
+
1559
+ // Create new selection instance
1560
+ this.selection = new ISelection(this, cursor.position, cursor.line);
1561
+ }
1562
+
1563
+ deleteSelection( cursor ) {
1564
+
1565
+ if(this.disable_edition)
1566
+ return;
1567
+ // Some selections don't depend on mouse up..
1568
+ if(this.selection) this.selection.invertIfNecessary();
1569
+
1570
+ const separator = "_NEWLINE_";
1571
+ let code = this.code.lines.join(separator);
1572
+
1573
+ // Get linear start index
1574
+ let index = 0;
1575
+ for(let i = 0; i <= this.selection.fromY; i++)
1576
+ index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[i].length);
1577
+
1578
+ index += this.selection.fromY * separator.length;
1579
+
1580
+ const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
1581
+ const pre = code.slice(0, index);
1582
+ const post = code.slice(index + num_chars);
1583
+
1584
+ this.code.lines = (pre + post).split(separator);
1585
+ this.processLines(this.selection.fromY);
1586
+
1587
+ this.cursorToLine(cursor, this.selection.fromY, true);
1588
+ this.cursorToPosition(cursor, this.selection.fromX);
1589
+ this.endSelection();
1590
+
1591
+ this._refresh_code_info(cursor.line, cursor.position);
1592
+ }
1593
+
1594
+ endSelection() {
1595
+
1596
+ this.selections.classList.remove('show');
1597
+ this.selections.innerHTML = "";
1598
+ delete this.selection;
1599
+ }
1600
+
1601
+ cursorToRight( key, cursor ) {
1602
+
1603
+ if(!key) return;
1604
+ cursor = cursor ?? this.cursors.children[0];
1605
+ cursor._left += this.charWidth;
1606
+ cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
1607
+ cursor.position++;
1608
+ this.restartBlink();
1609
+ this._refresh_code_info( cursor.line + 1, cursor.position );
1610
+
1611
+ // Add horizontal scroll
1612
+
1613
+ doAsync(() => {
1614
+ var last_char = ((this.code.scrollLeft + this.code.clientWidth) / this.charWidth)|0;
1615
+ if( cursor.position >= last_char )
1616
+ this.code.scrollLeft += this.charWidth;
1617
+ });
1618
+ }
1619
+
1620
+ cursorToLeft( key, cursor ) {
1621
+
1622
+ if(!key) return;
1623
+ cursor = cursor ?? this.cursors.children[0];
1624
+ cursor._left -= this.charWidth;
1625
+ cursor._left = Math.max(cursor._left, 0);
1626
+ cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
1627
+ cursor.position--;
1628
+ cursor.position = Math.max(cursor.position, 0);
1629
+ this.restartBlink();
1630
+ this._refresh_code_info( cursor.line + 1, cursor.position );
1631
+
1632
+ doAsync(() => {
1633
+ var first_char = (this.code.scrollLeft / this.charWidth)|0;
1634
+ if( (cursor.position - 1) < first_char )
1635
+ this.code.scrollLeft -= this.charWidth;
1636
+ });
1637
+ }
1638
+
1639
+ cursorToTop( cursor, resetLeft = false ) {
1640
+
1641
+ cursor = cursor ?? this.cursors.children[0];
1642
+ cursor._top -= this.lineHeight;
1643
+ cursor._top = Math.max(cursor._top, 4);
1644
+ cursor.style.top = "calc(" + (cursor._top - this.getScrollTop()) + "px)";
1645
+ this.restartBlink();
1646
+
1647
+ if(resetLeft)
1648
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1649
+
1650
+ this._refresh_code_info( cursor.line + 1, cursor.position );
1651
+
1652
+ doAsync(() => {
1653
+ var first_line = (this.code.scrollTop / this.lineHeight)|0;
1654
+ if( (cursor.line - 1) < first_line )
1655
+ this.code.scrollTop -= this.lineHeight;
1656
+ });
1657
+ }
1658
+
1659
+ cursorToBottom( cursor, resetLeft = false ) {
1660
+
1661
+ cursor = cursor ?? this.cursors.children[0];
1662
+ cursor._top += this.lineHeight;
1663
+ cursor.style.top = "calc(" + (cursor._top - this.getScrollTop()) + "px)";
1664
+ this.restartBlink();
1665
+
1666
+ if(resetLeft)
1667
+ this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1668
+
1669
+ this._refresh_code_info( cursor.line + 1, cursor.position );
1670
+
1671
+ doAsync(() => {
1672
+ var last_line = ((this.code.scrollTop + this.code.offsetHeight) / this.lineHeight)|0;
1673
+ if( cursor.line >= last_line )
1674
+ this.code.scrollTop += this.lineHeight;
1675
+ });
1676
+ }
1677
+
1678
+ cursorToString( cursor, text, reverse ) {
1679
+
1680
+ cursor = cursor ?? this.cursors.children[0];
1681
+ for( let char of text )
1682
+ reverse ? this.cursorToLeft(char) : this.cursorToRight(char);
1683
+ }
1684
+
1685
+ cursorToPosition( cursor, position ) {
1686
+
1687
+ cursor.position = position;
1688
+ cursor._left = position * this.charWidth;
1689
+ cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
1690
+ }
1691
+
1692
+ cursorToLine( cursor, line, resetLeft = false ) {
1693
+
1694
+ cursor.line = line;
1695
+ cursor._top = 4 + this.lineHeight * line;
1696
+ cursor.style.top = (cursor._top - this.getScrollTop()) + "px";
1697
+ if(resetLeft) this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1698
+ }
1699
+
1700
+ saveCursor( cursor, state = {} ) {
1701
+
1702
+ var cursor = cursor ?? this.cursors.children[0];
1703
+ state.top = cursor._top;
1704
+ state.left = cursor._left;
1705
+ state.line = cursor.line;
1706
+ state.charPos = cursor.position;
1707
+ return state;
1708
+ }
1709
+
1710
+ restoreCursor( cursor, state ) {
1711
+
1712
+ cursor = cursor ?? this.cursors.children[0];
1713
+ cursor.line = state.line ?? 0;
1714
+ cursor.position = state.charPos ?? 0;
1715
+
1716
+ cursor._left = state.left ?? 0;
1717
+ cursor.style.left = "calc(" + (cursor._left - this.getScrollLeft()) + "px + " + this.xPadding + ")";
1718
+ cursor._top = state.top ?? 4;
1719
+ cursor.style.top = "calc(" + (cursor._top - this.getScrollTop()) + "px)";
1720
+ }
1721
+
1722
+ resetCursorPos( flag, cursor ) {
1723
+
1724
+ cursor = cursor ?? this.cursors.children[0];
1725
+
1726
+ if( flag & CodeEditor.CURSOR_LEFT )
1727
+ {
1728
+ cursor._left = 0;
1729
+ cursor.style.left = "calc(" + (-this.getScrollLeft()) + "px + " + this.xPadding + ")";
1730
+ cursor.position = 0;
1731
+ }
1732
+
1733
+ if( flag & CodeEditor.CURSOR_TOP )
1734
+ {
1735
+ cursor._top = 4;
1736
+ cursor.style.top = (cursor._top - this.getScrollTop()) + "px";
1737
+ cursor.line = 0;
1738
+ }
1739
+ }
1740
+
1741
+ addSpaceTabs(n) {
1742
+
1743
+ for( var i = 0; i < n; ++i ) {
1744
+ this.actions['Tab'].callback();
1745
+ }
1746
+ }
1747
+
1748
+ addSpaces(n) {
1749
+
1750
+ for( var i = 0; i < n; ++i ) {
1751
+ this.root.dispatchEvent(new CustomEvent('keydown', {'detail': {
1752
+ skip_undo: true,
1753
+ key: ' '
1754
+ }}));
1755
+ }
1756
+ }
1757
+
1758
+ getScrollLeft() {
1759
+
1760
+ if(!this.code) return 0;
1761
+ return this.code.scrollLeft;
1762
+ }
1763
+
1764
+ getScrollTop() {
1765
+
1766
+ if(!this.code) return 0;
1767
+ return this.code.scrollTop;
1768
+ }
1769
+
1770
+ getCharAtPos( cursor, offset = 0) {
1771
+
1772
+ cursor = cursor ?? this.cursors.children[0];
1773
+ return this.code.lines[cursor.line][cursor.position + offset];
1774
+ }
1775
+
1776
+ getWordAtPos( cursor, offset = 0) {
1777
+
1778
+ cursor = cursor ?? this.cursors.children[0];
1779
+ const col = cursor.line;
1780
+ const words = this.code.lines[col];
1781
+
1782
+ const is_char = (char) => {
1783
+ const exceptions = ['_'];
1784
+ const code = char.charCodeAt(0);
1785
+ return (exceptions.indexOf(char) > - 1) || (code > 47 && code < 58) || (code > 64 && code < 91) || (code > 96 && code < 123);
1786
+ }
1787
+
1788
+ let it = cursor.position + offset;
1789
+
1790
+ while( words[it] && is_char(words[it]) )
1791
+ it--;
1792
+
1793
+ const from = it + 1;
1794
+ it = cursor.position + offset;
1795
+
1796
+ while( words[it] && is_char(words[it]) )
1797
+ it++;
1798
+
1799
+ const to = it;
1800
+
1801
+ return [words.substring( from, to ), from, to];
1802
+ }
1803
+
1804
+ measureChar(char = "a", get_bb = false) {
1805
+
1806
+ var test = document.createElement("pre");
1807
+ test.className = "codechar";
1808
+ test.innerHTML = char;
1809
+ document.body.appendChild(test);
1810
+ var rect = test.getBoundingClientRect();
1811
+ test.remove();
1812
+ const bb = [Math.floor(rect.width), Math.floor(rect.height)];
1813
+ return get_bb ? bb : bb[0];
1814
+ }
1815
+
1816
+ measureString(str) {
1817
+
1818
+ return str.length * this.charWidth;
1819
+ }
1820
+
1821
+ runScript( code ) {
1822
+ var script = document.createElement('script');
1823
+ script.type = 'text/javascript';
1824
+ script.innerHTML = code;
1825
+ // script.src = url[i] + ( version ? "?version=" + version : "" );
1826
+ script.async = false;
1827
+ // script.onload = function(e) { };
1828
+ document.getElementsByTagName('head')[0].appendChild(script);
1829
+ }
1830
+
1831
+ toJSONFormat(text) {
1832
+
1833
+ let params = text.split(":");
1834
+ for(let i = 0; i < params.length; i++) {
1835
+ let key = params[i].split(',');
1836
+ if(key.length > 1) {
1837
+ if(key[key.length-1].includes("]"))
1838
+ continue;
1839
+ key = key[key.length-1];
1840
+ }
1841
+ else if(key[0].includes("}"))
1842
+ continue;
1843
+ else
1844
+ key = key[0];
1845
+ key = key.replaceAll(/[{}\n\r]/g,"").replaceAll(" ","")
1846
+ if(key[0] != '"' && key[key.length - 1] != '"') {
1847
+ params[i] = params[i].replace(key, '"' + key + '"');
1848
+ }
1849
+ }
1850
+ text = params.join(':');
1851
+
1852
+ try {
1853
+ let json = JSON.parse(text);
1854
+ return JSON.stringify(json, undefined, 4);
1855
+ }
1856
+ catch(e) {
1857
+ alert("Invalid JSON format");
1858
+ return;
1859
+ }
1860
+ }
1861
+ }
1862
+
1863
+ LX.CodeEditor = CodeEditor;
1864
+
1865
+ export { CodeEditor };