lexgui 0.1.23 → 0.1.25

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.
@@ -26,7 +26,8 @@ function sliceChars( str, idx, n = 1 ) {
26
26
  }
27
27
 
28
28
  function firstNonspaceIndex( str ) {
29
- return str.search(/\S|$/);
29
+ const index = str.search(/\S|$/)
30
+ return index < str.length ? index : -1;
30
31
  }
31
32
 
32
33
  function indexOfFrom( str, reg, from, reverse ) {
@@ -118,7 +119,7 @@ class CodeSelection {
118
119
  this.editor.selections.appendChild(domEl);
119
120
 
120
121
  // Hide active line background
121
- this.editor.code.childNodes.forEach( e => e.classList.remove( 'active-line' ) );
122
+ this.editor._hideActiveLine();
122
123
  }
123
124
  };
124
125
 
@@ -201,8 +202,8 @@ class CodeEditor {
201
202
  static WORD_TYPE_METHOD = 0;
202
203
  static WORD_TYPE_CLASS = 1;
203
204
 
204
- static CODE_MAX_FONT_SIZE = 22;
205
205
  static CODE_MIN_FONT_SIZE = 9;
206
+ static CODE_MAX_FONT_SIZE = 22;
206
207
 
207
208
  /**
208
209
  * @param {*} options
@@ -333,7 +334,7 @@ class CodeEditor {
333
334
 
334
335
  // Add main cursor
335
336
  {
336
- this._addCursor( 0, 0, true );
337
+ this._addCursor( 0, 0, true, true );
337
338
 
338
339
  Object.defineProperty( this, 'line', {
339
340
  get: (v) => { return this._getCurrentCursor().line }
@@ -493,7 +494,8 @@ class CodeEditor {
493
494
  this.state = {
494
495
  focused: false,
495
496
  selectingText: false,
496
- activeLine: null
497
+ activeLine: null,
498
+ keyChain: null
497
499
  }
498
500
 
499
501
  // Code
@@ -550,84 +552,16 @@ class CodeEditor {
550
552
  'End', 'Tab', 'Escape'
551
553
  ];
552
554
 
553
- this.keywords = {
554
- 'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
555
- 'arguments', 'extends', 'instanceof'],
556
- 'C++': ['int', 'float', 'double', 'bool', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'nullptr',
557
- 'NULL', 'unsigned', 'namespace'],
558
- 'JSON': ['true', 'false'],
559
- 'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
560
- 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
561
- 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
562
- 'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
563
- 'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
564
- 'texture_storage_2d_array', 'texture_storage_3d'],
565
- 'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
566
- 'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
567
- 'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
568
- 'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
569
- 'DRIVERQUERY', 'print', 'PRINT'],
570
- 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
571
- 'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
572
- };
573
- this.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
574
- 'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
575
- 'bind', 'prototype', 'length', 'assign', 'entries', 'values', 'concat', 'substring', 'substr', 'splice', 'slice', 'buffer', 'appendChild', 'createElement', 'prompt',
576
- 'alert'],
577
- 'WGSL': ['textureSample'],
578
- 'Python': ['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
579
- 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance',
580
- 'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr',
581
- 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
582
- };
583
- this.types = {
584
- 'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder'],
585
- 'Rust': ['u128'],
586
- 'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
587
- 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
588
- 'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
589
- 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
590
- 'C++': ['uint8_t', 'uint16_t', 'uint32_t']
591
- };
592
- this.builtin = {
593
- 'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
594
- 'CSS': ['*', '!important'],
595
- 'C++': ['vector', 'list', 'map'],
596
- 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
597
- 'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
598
- };
599
- this.statementsAndDeclarations = {
600
- 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
601
- 'CSS': ['@', 'import'],
602
- 'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog'],
603
- 'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
604
- 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'uniform'],
605
- 'Rust': ['break', 'else', 'continue', 'for', 'if', 'loop', 'match', 'return', 'while', 'do', 'yield'],
606
- 'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue',
607
- 'global', 'pass'],
608
- 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT']
609
- };
610
- this.symbols = {
611
- 'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
612
- 'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
613
- 'JSON': ['[', ']', '{', '}', '(', ')'],
614
- 'GLSL': ['[', ']', '{', '}', '(', ')'],
615
- 'WGSL': ['[', ']', '{', '}', '(', ')', '->'],
616
- 'CSS': ['{', '}', '(', ')', '*'],
617
- 'Rust': ['<', '>', '[', ']', '(', ')', '='],
618
- 'Python': ['<', '>', '[', ']', '(', ')', '='],
619
- 'Batch': ['[', ']', '(', ')', '%'],
620
- 'HTML': ['<', '>', '/']
621
- };
555
+ this._blockCommentCache = [];
622
556
 
623
557
  // Convert reserved word arrays to maps so we can search tokens faster
624
558
 
625
- for( let lang in this.keywords ) this.keywords[lang] = this.keywords[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
626
- for( let lang in this.utils ) this.utils[lang] = this.utils[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
627
- for( let lang in this.types ) this.types[lang] = this.types[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
628
- for( let lang in this.builtin ) this.builtin[lang] = this.builtin[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
629
- for( let lang in this.statementsAndDeclarations ) this.statementsAndDeclarations[lang] = this.statementsAndDeclarations[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
630
- for( let lang in this.symbols ) this.symbols[lang] = this.symbols[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
559
+ for( let lang in CodeEditor.keywords ) CodeEditor.keywords[lang] = CodeEditor.keywords[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
560
+ for( let lang in CodeEditor.utils ) CodeEditor.utils[lang] = CodeEditor.utils[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
561
+ for( let lang in CodeEditor.types ) CodeEditor.types[lang] = CodeEditor.types[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
562
+ for( let lang in CodeEditor.builtin ) CodeEditor.builtin[lang] = CodeEditor.builtin[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
563
+ for( let lang in CodeEditor.statementsAndDeclarations ) CodeEditor.statementsAndDeclarations[lang] = CodeEditor.statementsAndDeclarations[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
564
+ for( let lang in CodeEditor.symbols ) CodeEditor.symbols[lang] = CodeEditor.symbols[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
631
565
 
632
566
  // Action keys
633
567
 
@@ -739,14 +673,18 @@ class CodeEditor {
739
673
  if(idx > 0) this.cursorToString( cursor, prestring );
740
674
  this.setScrollLeft( 0 );
741
675
 
676
+ // Merge cursors
677
+ this.mergeCursors( ln );
678
+
742
679
  if( e.shiftKey && !e.cancelShift )
743
680
  {
744
681
  // Get last selection range
745
682
  if( this.selection )
746
- lastX += this.selection.chars;
683
+ lastX += this.selection.chars;
747
684
 
748
685
  if( !this.selection )
749
686
  this.startSelection( cursor );
687
+
750
688
  var string = this.code.lines[ ln ].substring( idx, lastX );
751
689
  if( this.selection.sameLine() )
752
690
  this.selection.selectInline( idx, cursor.line, this.measureString( string ) );
@@ -781,6 +719,9 @@ class CodeEditor {
781
719
 
782
720
  const last_char = ( this.code.clientWidth / this.charWidth )|0;
783
721
  this.setScrollLeft( cursor.position >= last_char ? ( cursor.position - last_char ) * this.charWidth : 0 );
722
+
723
+ // Merge cursors
724
+ this.mergeCursors( ln );
784
725
  });
785
726
 
786
727
  this.action( 'Enter', true, ( ln, cursor, e ) => {
@@ -806,6 +747,7 @@ class CodeEditor {
806
747
  this.code.lines.splice( cursor.line + 1, 0, "" );
807
748
  this.code.lines[cursor.line + 1] = this.code.lines[ ln ].substr( cursor.position ); // new line (below)
808
749
  this.code.lines[ ln ] = this.code.lines[ ln ].substr( 0, cursor.position ); // line above
750
+
809
751
  this.lineDown( cursor, true );
810
752
 
811
753
  // Check indentation
@@ -864,24 +806,20 @@ class CodeEditor {
864
806
  if( e.shiftKey ) {
865
807
  if( !this.selection )
866
808
  this.startSelection( cursor );
809
+ } else {
810
+ this.endSelection();
811
+ }
867
812
 
868
- this.lineDown( cursor );
869
-
870
- var letter = this.getCharAtPos( cursor );
871
- if( !letter ) {
872
- this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length - 1, 0) );
873
- }
813
+ const canGoDown = this.lineDown( cursor );
814
+ const letter = this.getCharAtPos( cursor );
815
+
816
+ // Go to end of line if out of range
817
+ if( !letter || !canGoDown ) {
818
+ this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
819
+ }
874
820
 
821
+ if( e.shiftKey ) {
875
822
  this.processSelection( e );
876
- } else {
877
-
878
- if( this.code.lines[ ln + 1 ] == undefined )
879
- return;
880
- this.endSelection();
881
- this.lineDown( cursor );
882
- // Go to end of line if out of line
883
- var letter = this.getCharAtPos( cursor );
884
- if( !letter ) this.actions['End'].callback(cursor.line, cursor, e);
885
823
  }
886
824
  }
887
825
  // Move down autocomplete selection
@@ -1212,25 +1150,36 @@ class CodeEditor {
1212
1150
  }
1213
1151
  }
1214
1152
 
1215
- _addCursor( line = 0, position = 0, isMain = false ) {
1153
+ _addCursor( line = 0, position = 0, force, isMain = false ) {
1154
+
1155
+ // If cursor in that position exists, remove it instead..
1156
+ const exists = Array.from( this.cursors.children ).find( v => v.position == position && v.line == line );
1157
+ if( exists && !force )
1158
+ {
1159
+ if( !exists.isMain )
1160
+ exists.remove();
1216
1161
 
1162
+ return;
1163
+ }
1164
+
1217
1165
  let cursor = document.createElement( 'div' );
1218
1166
  cursor.className = "cursor";
1219
1167
  cursor.innerHTML = "&nbsp;";
1220
- cursor.isMainCursor = isMain;
1168
+ cursor.isMain = isMain;
1221
1169
  cursor._left = position * this.charWidth;
1222
1170
  cursor.style.left = "calc( " + cursor._left + "px + " + this.xPadding + " )";
1223
1171
  cursor._top = line * this.lineHeight;
1224
1172
  cursor.style.top = cursor._top + "px";
1225
1173
  cursor._position = position;
1226
1174
  cursor._line = line;
1227
- cursor.print = (function() { console.log( this, this._line, this._position ) }).bind( cursor );
1175
+ cursor.print = (function() { console.log( this._line, this._position ) }).bind( cursor );
1176
+ cursor.isLast = (function() { return this.cursors.lastChild == cursor; }).bind( this );
1228
1177
 
1229
1178
  Object.defineProperty( cursor, 'line', {
1230
1179
  get: (v) => { return cursor._line },
1231
1180
  set: (v) => {
1232
1181
  cursor._line = v;
1233
- if( cursor.isMainCursor ) this._setActiveLine( v );
1182
+ if( cursor.isMain ) this._setActiveLine( v );
1234
1183
  }
1235
1184
  } );
1236
1185
 
@@ -1238,7 +1187,7 @@ class CodeEditor {
1238
1187
  get: (v) => { return cursor._position },
1239
1188
  set: (v) => {
1240
1189
  cursor._position = v;
1241
- if( cursor.isMainCursor ) this._updateDataInfoPanel( "@cursor-pos", "Col " + v );
1190
+ if( cursor.isMain ) this._updateDataInfoPanel( "@cursor-pos", "Col " + v );
1242
1191
  }
1243
1192
  } );
1244
1193
 
@@ -1273,6 +1222,10 @@ class CodeEditor {
1273
1222
 
1274
1223
  _addUndoStep( cursor, force, deleteRedo = true ) {
1275
1224
 
1225
+ // Only the mainc cursor stores undo steps
1226
+ if( !cursor.isMain )
1227
+ return;
1228
+
1276
1229
  const d = new Date();
1277
1230
  const current = d.getTime();
1278
1231
 
@@ -1281,7 +1234,7 @@ class CodeEditor {
1281
1234
  if( !this._lastTime ) {
1282
1235
  this._lastTime = current;
1283
1236
  } else {
1284
- if( ( current - this._lastTime ) > 3000 ){
1237
+ if( ( current - this._lastTime ) > 2000 ){
1285
1238
  this._lastTime = null;
1286
1239
  } else {
1287
1240
  // If time not enough, reset timer
@@ -1299,22 +1252,80 @@ class CodeEditor {
1299
1252
 
1300
1253
  this.code.undoSteps.push( {
1301
1254
  lines: LX.deepCopy( this.code.lines ),
1302
- cursor: this.saveCursor( cursor ),
1303
- line: cursor.line,
1304
- position: cursor.position
1255
+ cursors: this.saveCursors()
1305
1256
  } );
1306
1257
  }
1307
1258
 
1259
+ _doUndo( cursor ) {
1260
+
1261
+ if( !this.code.undoSteps.length )
1262
+ return;
1263
+
1264
+ this._addRedoStep( cursor );
1265
+
1266
+ // Extract info from the last code state
1267
+ const step = this.code.undoSteps.pop();
1268
+
1269
+ // Set old state lines
1270
+ this.code.lines = step.lines;
1271
+ this.processLines();
1272
+
1273
+ this._removeSecondaryCursors();
1274
+
1275
+ for( let i = 0; i < step.cursors.length; ++i )
1276
+ {
1277
+ var currentCursor = this.cursors.children[ i ];
1278
+
1279
+ // Generate new if needed
1280
+ if( !currentCursor )
1281
+ currentCursor = this._addCursor();
1282
+
1283
+ this.restoreCursor( currentCursor, step.cursors[ i ] );
1284
+ }
1285
+
1286
+ this._hideActiveLine();
1287
+ }
1288
+
1308
1289
  _addRedoStep( cursor ) {
1309
1290
 
1291
+ // Only the mainc cursor stores redo steps
1292
+ if( !cursor.isMain )
1293
+ return;
1294
+
1310
1295
  this.code.redoSteps.push( {
1311
1296
  lines: LX.deepCopy( this.code.lines ),
1312
- cursor: this.saveCursor( cursor ),
1313
- line: cursor.line,
1314
- position: cursor.position
1297
+ cursors: this.saveCursors()
1315
1298
  } );
1316
1299
  }
1317
1300
 
1301
+ _doRedo( cursor ) {
1302
+
1303
+ if( !this.code.redoSteps.length )
1304
+ return;
1305
+
1306
+ this._addUndoStep( cursor, true, false);
1307
+
1308
+ // Extract info from the next saved code state
1309
+ const step = this.code.redoSteps.pop();
1310
+
1311
+ // Set old state lines
1312
+ this.code.lines = step.lines;
1313
+ this.processLines();
1314
+
1315
+ this._removeSecondaryCursors();
1316
+
1317
+ for( let i = 0; i < step.cursors.length; ++i )
1318
+ {
1319
+ var currentCursor = this.cursors.children[ i ];
1320
+
1321
+ // Generate new if needed
1322
+ if( !currentCursor )
1323
+ currentCursor = this._addCursor();
1324
+
1325
+ this.restoreCursor( currentCursor, step.cursors[ i ] );
1326
+ }
1327
+ }
1328
+
1318
1329
  _changeLanguage( lang ) {
1319
1330
 
1320
1331
  this.code.language = lang;
@@ -1610,7 +1621,7 @@ class CodeEditor {
1610
1621
 
1611
1622
  processMouse( e ) {
1612
1623
 
1613
- if( !e.target.classList.contains('code') ) return;
1624
+ if( !e.target.classList.contains('code') && !e.target.classList.contains('codetabsarea') ) return;
1614
1625
  if( !this.code ) return;
1615
1626
 
1616
1627
  var cursor = this._getCurrentCursor();
@@ -1618,11 +1629,8 @@ class CodeEditor {
1618
1629
  var mouse_pos = [(e.clientX - code_rect.x), (e.clientY - code_rect.y)];
1619
1630
 
1620
1631
  // Discard out of lines click...
1621
- if( e.type != 'contextmenu' )
1622
- {
1623
- var ln = (mouse_pos[1] / this.lineHeight)|0;
1624
- if(this.code.lines[ ln ] == undefined) return;
1625
- }
1632
+ var ln = ( mouse_pos[ 1 ] / this.lineHeight ) | 0;
1633
+ if( ln < 0 ) return;
1626
1634
 
1627
1635
  if( e.type == 'mousedown' )
1628
1636
  {
@@ -1730,11 +1738,12 @@ class CodeEditor {
1730
1738
  var position = [( e.clientX - code_rect.x ) + this.getScrollLeft(), (e.clientY - code_rect.y) + this.getScrollTop()];
1731
1739
  var ln = (position[ 1 ] / this.lineHeight)|0;
1732
1740
 
1733
- if( this.code.lines[ ln ] == undefined )
1734
- return;
1741
+ // Check out of range line
1742
+ const outOfRange = ln > this.code.lines.length - 1;
1743
+ ln = Math.min( ln, this.code.lines.length - 1 );
1735
1744
 
1736
1745
  var ch = ( ( position[ 0 ] - parseInt( this.xPadding ) + 3) / this.charWidth )|0;
1737
- var string = this.code.lines[ ln ].slice( 0, ch );
1746
+ var string = outOfRange ? this.code.lines[ ln ] : this.code.lines[ ln ].slice( 0, ch );
1738
1747
 
1739
1748
  // Move main cursor there...
1740
1749
  if( !e.altKey )
@@ -1764,8 +1773,7 @@ class CodeEditor {
1764
1773
  if( !this.selection )
1765
1774
  this.startSelection( cursor );
1766
1775
 
1767
- // Hide active line background
1768
- this.code.childNodes.forEach( e => e.classList.remove( 'active-line' ) );
1776
+ this._hideActiveLine();
1769
1777
 
1770
1778
  // Update selection
1771
1779
  if( !keep_range )
@@ -1911,10 +1919,36 @@ class CodeEditor {
1911
1919
  }
1912
1920
  }
1913
1921
 
1914
- async processKey( event ) {
1922
+ async processKey( e ) {
1915
1923
 
1916
1924
  const numCursors = this.cursors.childElementCount;
1917
1925
 
1926
+ if( !this.code || e.srcElement.constructor != HTMLDivElement )
1927
+ return;
1928
+
1929
+ const key = e.key ?? e.detail.key;
1930
+ const target = e.detail.targetCursor;
1931
+
1932
+ // Global keys
1933
+
1934
+ if( this._processGlobalKeys( e, key ) ) {
1935
+ // Stop propagation..
1936
+ return;
1937
+ }
1938
+
1939
+ // By cursor keys
1940
+
1941
+ if( target !== undefined )
1942
+ {
1943
+ this.processKeyAtTargetCursor( e, key, target );
1944
+ return;
1945
+ }
1946
+
1947
+ this._lastProcessedCursorIndex = null;
1948
+
1949
+ var lastProcessedCursor = null;
1950
+ var cursorOffset = new LX.vec2( 0, 0 );
1951
+
1918
1952
  for( var i = 0; i < numCursors; i++ )
1919
1953
  {
1920
1954
  let cursor = this.cursors.children[ i ];
@@ -1923,16 +1957,97 @@ class CodeEditor {
1923
1957
  if( !cursor )
1924
1958
  break;
1925
1959
 
1926
- this.processKeyAtCursor( event, cursor );
1960
+ // Arrows don't modify code lines.. And only add offset if in the same line
1961
+ if( lastProcessedCursor && lastProcessedCursor.line == cursor.line && !key.includes( 'Arrow' ) )
1962
+ {
1963
+ cursor.position += cursorOffset.x;
1964
+ cursor.line += cursorOffset.y;
1965
+
1966
+ this.relocateCursors();
1967
+ }
1968
+
1969
+ lastProcessedCursor = this.saveCursor( cursor );
1970
+ this._lastProcessedCursorIndex = i;
1971
+
1972
+ this._processKeyAtCursor( e, key, cursor );
1973
+
1974
+ cursorOffset.x += ( cursor.position - lastProcessedCursor.position );
1975
+ cursorOffset.y += ( cursor.line - lastProcessedCursor.line );
1927
1976
  }
1977
+
1978
+ // Clear tmp
1979
+
1980
+ delete this._lastProcessedCursorIndex;
1928
1981
  }
1929
1982
 
1930
- async processKeyAtCursor( e, cursor ) {
1983
+ async processKeyAtTargetCursor( e, key, targetIdx ) {
1931
1984
 
1932
- if( !this.code || e.srcElement.constructor != HTMLDivElement )
1985
+ let cursor = this.cursors.children[ targetIdx ];
1986
+
1987
+ // We could delete secondary cursor while iterating..
1988
+ if( !cursor )
1933
1989
  return;
1934
1990
 
1935
- var key = e.key ?? e.detail.key;
1991
+ this._processKeyAtCursor( e, key, cursor );
1992
+ this._processGlobalKeys( e, key );
1993
+ }
1994
+
1995
+ _processGlobalKeys( e, key ) {
1996
+
1997
+ let cursor = this._getCurrentCursor();
1998
+
1999
+ if( e.ctrlKey || e.metaKey )
2000
+ {
2001
+ e.preventDefault();
2002
+
2003
+ switch( key.toLowerCase() ) {
2004
+ case 'a': // select all
2005
+ this.selectAll();
2006
+ return true;
2007
+ case 'c': // k+c, comment line
2008
+ if( this.state.keyChain == 'k' ) {
2009
+ this._commentLines();
2010
+ return true;
2011
+ }
2012
+ return false;
2013
+ case 'f': // find/search
2014
+ this.showSearchBox();
2015
+ return true;
2016
+ case 'g': // find line
2017
+ this.showSearchLineBox();
2018
+ return true;
2019
+ case 'k': // shortcut chain
2020
+ this.state.keyChain = 'k';
2021
+ return true;
2022
+ case 's': // save
2023
+ this.onsave( this.getText() );
2024
+ return true;
2025
+ case 'u': // k+u, uncomment line
2026
+ if( this.state.keyChain == 'k' ) {
2027
+ this._uncommentLines();
2028
+ return true;
2029
+ }
2030
+ return false;
2031
+ case 'y': // redo
2032
+ this._doRedo( cursor );
2033
+ return true;
2034
+ case 'z': // undo
2035
+ this._doUndo( cursor );
2036
+ return true;
2037
+ case '+': // increase size
2038
+ this._increaseFontSize();
2039
+ return true;
2040
+ case '-': // decrease size
2041
+ this._decreaseFontSize();
2042
+ return true;
2043
+ }
2044
+ }
2045
+
2046
+ this.state.keyChain = null;
2047
+ return false;
2048
+ }
2049
+
2050
+ async _processKeyAtCursor( e, key, cursor ) {
1936
2051
 
1937
2052
  const skip_undo = e.detail.skip_undo ?? false;
1938
2053
 
@@ -1945,32 +2060,20 @@ class CodeEditor {
1945
2060
 
1946
2061
  // Check combinations
1947
2062
 
2063
+ const isLastCursor = cursor.isLast();
2064
+
1948
2065
  if( e.ctrlKey || e.metaKey )
1949
2066
  {
1950
2067
  switch( key.toLowerCase() ) {
1951
- case 'a': // select all
1952
- e.preventDefault();
1953
- this.selectAll( cursor );
1954
- break;
1955
2068
  case 'c': // copy
2069
+ // TODO: COPY TEXT FROM EVERY CURSOR
1956
2070
  this._copyContent( cursor );
1957
2071
  return;
1958
2072
  case 'd': // duplicate line
1959
2073
  e.preventDefault();
2074
+ // TODO: UPDATE NEXT CURSOR ON MODIFY STATE
1960
2075
  this._duplicateLine( lidx, cursor );
1961
2076
  return;
1962
- case 'f': // find/search
1963
- e.preventDefault();
1964
- this.showSearchBox();
1965
- return;
1966
- case 'g': // find line
1967
- e.preventDefault();
1968
- this.showSearchLineBox();
1969
- return;
1970
- case 's': // save
1971
- e.preventDefault();
1972
- this.onsave( this.getText() );
1973
- return;
1974
2077
  case 'v': // paste
1975
2078
  this._pasteContent( cursor );
1976
2079
  return;
@@ -1978,39 +2081,13 @@ class CodeEditor {
1978
2081
  this._cutContent( cursor );
1979
2082
  this.hideAutoCompleteBox();
1980
2083
  return;
1981
- case 'y': // redo
1982
- if( !this.code.redoSteps.length )
1983
- return;
1984
- this._addUndoStep( cursor, true, false);
1985
- const redo_step = this.code.redoSteps.pop();
1986
- this.code.lines = redo_step.lines;
1987
- this.processLines();
1988
- this.restoreCursor( cursor, redo_step.cursor );
1989
- return;
1990
- case 'z': // undo
1991
- if( !this.code.undoSteps.length )
1992
- return;
1993
- this._addRedoStep( cursor );
1994
- const undo_step = this.code.undoSteps.pop();
1995
- this.code.lines = undo_step.lines;
1996
- this.processLines();
1997
- this.restoreCursor( cursor, undo_step.cursor );
1998
- return;
1999
- case '+': // increase size
2000
- e.preventDefault();
2001
- this._increaseFontSize();
2002
- return;
2003
- case '-': // decrease size
2004
- e.preventDefault();
2005
- this._decreaseFontSize();
2006
- return;
2007
2084
  case 'arrowdown': // add cursor below only for the main cursor..
2008
- if( cursor.isMainCursor && this.code.lines[ lidx + 1 ] != undefined )
2085
+ if( isLastCursor && this.code.lines[ lidx + 1 ] != undefined )
2009
2086
  {
2010
- var new_cursor = this._addCursor( cursor.line, cursor.position );
2087
+ var new_cursor = this._addCursor( cursor.line, cursor.position, true );
2011
2088
  this.lineDown( new_cursor );
2012
- return;
2013
2089
  }
2090
+ return;
2014
2091
  }
2015
2092
  }
2016
2093
 
@@ -2136,6 +2213,9 @@ class CodeEditor {
2136
2213
  async _pasteContent( cursor ) {
2137
2214
 
2138
2215
  let text = await navigator.clipboard.readText();
2216
+
2217
+ this._addUndoStep( cursor, true );
2218
+
2139
2219
  this.appendText( text, cursor );
2140
2220
  }
2141
2221
 
@@ -2152,18 +2232,18 @@ class CodeEditor {
2152
2232
  if( this.selection ) this.selection.invertIfNecessary();
2153
2233
 
2154
2234
  const separator = "_NEWLINE_";
2155
- let code = this.code.lines.join(separator);
2235
+ let code = this.code.lines.join( separator );
2156
2236
 
2157
2237
  // Get linear start index
2158
2238
  let index = 0;
2159
2239
 
2160
- for(let i = 0; i <= this.selection.fromY; i++)
2161
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
2240
+ for( let i = 0; i <= this.selection.fromY; i++ )
2241
+ index += ( i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length );
2162
2242
 
2163
2243
  index += this.selection.fromY * separator.length;
2164
- const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
2165
- const text = code.substr(index, num_chars);
2166
- const lines = text.split(separator);
2244
+ const num_chars = this.selection.chars + ( this.selection.toY - this.selection.fromY ) * separator.length;
2245
+ const text = code.substr( index, num_chars );
2246
+ const lines = text.split( separator );
2167
2247
  text_to_copy = lines.join('\n');
2168
2248
  }
2169
2249
 
@@ -2221,6 +2301,114 @@ class CodeEditor {
2221
2301
  this.hideAutoCompleteBox();
2222
2302
  }
2223
2303
 
2304
+ _commentLines() {
2305
+
2306
+ this.state.keyChain = null;
2307
+
2308
+ if( this.selection )
2309
+ {
2310
+ var cursor = this._getCurrentCursor();
2311
+ this._addUndoStep( cursor, true );
2312
+
2313
+ const selectedLines = this.code.lines.slice( this.selection.fromY, this.selection.toY );
2314
+ const minIdx = Math.min(...selectedLines.map( v => {
2315
+ var idx = firstNonspaceIndex( v );
2316
+ return idx < 0 ? 1e10 : idx;
2317
+ } ));
2318
+
2319
+ for( var i = this.selection.fromY; i <= this.selection.toY; ++i )
2320
+ {
2321
+ this._commentLine( cursor, i, minIdx );
2322
+ }
2323
+ }
2324
+ else
2325
+ {
2326
+ for( let cursor of this.cursors.children )
2327
+ {
2328
+ this._addUndoStep( cursor, true );
2329
+ this._commentLine( cursor, cursor.line );
2330
+ }
2331
+ }
2332
+
2333
+ this.processLines();
2334
+ this._hideActiveLine();
2335
+ }
2336
+
2337
+ _commentLine( cursor, line, minNonspaceIdx ) {
2338
+
2339
+ const lang = this.languages[ this.highlight ];
2340
+
2341
+ if( !( lang.singleLineComments ?? true ))
2342
+ return;
2343
+
2344
+ const token = ( lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken ) + ' ';
2345
+ const string = this.code.lines[ line ];
2346
+
2347
+ let idx = firstNonspaceIndex( string );
2348
+ if( idx > -1 )
2349
+ {
2350
+ // Update idx using min of the selected lines (if necessary..)
2351
+ idx = minNonspaceIdx ?? idx;
2352
+
2353
+ this.code.lines[ line ] = [
2354
+ string.substring( 0, idx ),
2355
+ token,
2356
+ string.substring( idx )
2357
+ ].join( '' );
2358
+
2359
+ this.cursorToString( cursor, token );
2360
+ }
2361
+ }
2362
+
2363
+ _uncommentLines() {
2364
+
2365
+ this.state.keyChain = null;
2366
+
2367
+ if( this.selection )
2368
+ {
2369
+ var cursor = this._getCurrentCursor();
2370
+ this._addUndoStep( cursor, true );
2371
+
2372
+ for( var i = this.selection.fromY; i <= this.selection.toY; ++i )
2373
+ {
2374
+ this._uncommentLine( cursor, i );
2375
+ }
2376
+ }
2377
+ else
2378
+ {
2379
+ for( let cursor of this.cursors.children )
2380
+ {
2381
+ this._addUndoStep( cursor, true );
2382
+ this._uncommentLine( cursor, cursor.line );
2383
+ }
2384
+ }
2385
+
2386
+ this.processLines();
2387
+ this._hideActiveLine();
2388
+ }
2389
+
2390
+ _uncommentLine( cursor, line ) {
2391
+
2392
+ const lang = this.languages[ this.highlight ];
2393
+
2394
+ if( !( lang.singleLineComments ?? true ))
2395
+ return;
2396
+
2397
+ const token = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2398
+ const string = this.code.lines[ line ];
2399
+
2400
+ if( string.includes( token ) )
2401
+ {
2402
+ this.code.lines[ line ] = string.replace( token + ' ', '' );
2403
+
2404
+ // try deleting token + space, and then if not, delete only the token
2405
+ if( string.length == this.code.lines[ line ].length )
2406
+ this.code.lines[ line ] = string.replace( token, '' );
2407
+
2408
+ this.cursorToString( cursor, 'X'.repeat( Math.abs( string.length - this.code.lines[ line ].length ) ), true );
2409
+ }
2410
+ }
2411
+
2224
2412
  action( key, deleteSelection, fn ) {
2225
2413
 
2226
2414
  this.actions[ key ] = {
@@ -2254,9 +2442,8 @@ class CodeEditor {
2254
2442
 
2255
2443
  processLines( mode ) {
2256
2444
 
2257
- const start = performance.now();
2258
-
2259
2445
  var code_html = "";
2446
+ this._blockCommentCache.length = 0;
2260
2447
 
2261
2448
  // Reset all lines content
2262
2449
  this.code.innerHTML = "";
@@ -2301,6 +2488,13 @@ class CodeEditor {
2301
2488
 
2302
2489
  processLine( linenum, force ) {
2303
2490
 
2491
+ // Check if we are in block comment sections..
2492
+ if( !force && this._inBlockCommentSection( linenum ) )
2493
+ {
2494
+ this.processLines();
2495
+ return;
2496
+ }
2497
+
2304
2498
  const lang = this.languages[ this.highlight ];
2305
2499
  const local_line_num = this.toLocalLine( linenum );
2306
2500
  const gutter_line = "<span class='line-gutter'>" + (linenum + 1) + "</span>";
@@ -2309,6 +2503,7 @@ class CodeEditor {
2309
2503
  if( !force ) // Single line update
2310
2504
  {
2311
2505
  this.code.childNodes[ local_line_num ].innerHTML = gutter_line + html;
2506
+ this._setActiveLine( linenum );
2312
2507
  this._clearTmpVariables();
2313
2508
  }
2314
2509
  else // Update all lines at once
@@ -2335,6 +2530,9 @@ class CodeEditor {
2335
2530
  return UPDATE_LINE( linestring );
2336
2531
  }
2337
2532
 
2533
+ this._currentLineNumber = linenum;
2534
+ this._currentLineString = linestring;
2535
+
2338
2536
  const tokensToEvaluate = this._getTokensFromLine( linestring );
2339
2537
 
2340
2538
  if( !tokensToEvaluate.length )
@@ -2365,7 +2563,7 @@ class CodeEditor {
2365
2563
  {
2366
2564
  const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
2367
2565
  if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
2368
- this._buildingBlockComment = true;
2566
+ this._buildingBlockComment = linenum;
2369
2567
  }
2370
2568
 
2371
2569
  line_inner_html += this._evaluateToken( {
@@ -2403,19 +2601,12 @@ class CodeEditor {
2403
2601
  var matches = (linestring.substring( 0, idx ).match( re ) || []);
2404
2602
  return (matches.length % 2) !== 0;
2405
2603
  } );
2406
- err |= stringKeys.some( function(v) {
2407
- var re = new RegExp( v, "g" );
2408
- var matches = (linestring.substring( idx ).match( re ) || []);
2409
- return (matches.length % 2) !== 0;
2410
- } );
2411
2604
  return err ? undefined : idx;
2412
2605
  }
2413
2606
  }
2414
2607
 
2415
2608
  _getTokensFromLine( linestring, skipNonWords ) {
2416
2609
 
2417
- this._currentLineString = linestring;
2418
-
2419
2610
  // Check if line comment
2420
2611
  const ogLine = linestring;
2421
2612
  const hasCommentIdx = this._lineHasComment( linestring );
@@ -2472,12 +2663,12 @@ class CodeEditor {
2472
2663
 
2473
2664
  if( this.highlight == 'C++' || this.highlight == 'CSS' )
2474
2665
  {
2475
- var idx = tokens.slice( offset ).findIndex( ( value, index ) => this.isNumber( value ) );
2666
+ var idx = tokens.slice( offset ).findIndex( ( value, index ) => this._isNumber( value ) );
2476
2667
  if( idx > -1 )
2477
2668
  {
2478
2669
  idx += offset; // Add offset to compute within the whole array of tokens
2479
2670
  let data = tokens[ idx ] + tokens[ ++idx ];
2480
- while( this.isNumber( data ) )
2671
+ while( this._isNumber( data ) )
2481
2672
  {
2482
2673
  tokens[ idx - 1 ] += tokens[ idx ];
2483
2674
  tokens.splice( idx, 1 );
@@ -2525,7 +2716,7 @@ class CodeEditor {
2525
2716
  // Manage strings
2526
2717
  this._stringEnded = false;
2527
2718
 
2528
- if( usePreviousTokenToCheckString || ( !this._buildingBlockComment && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
2719
+ if( usePreviousTokenToCheckString || ( this._buildingBlockComment === undefined && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
2529
2720
  {
2530
2721
  const checkIfStringEnded = t => {
2531
2722
  const idx = Object.values( customStringKeys ).indexOf( t );
@@ -2574,22 +2765,22 @@ class CodeEditor {
2574
2765
  else if( this._buildingString != undefined )
2575
2766
  discardToken = this._appendStringToken( token );
2576
2767
 
2577
- else if( this._mustHightlightWord( token, this.keywords ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2768
+ else if( this._mustHightlightWord( token, CodeEditor.keywords ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2578
2769
  token_classname = "cm-kwd";
2579
2770
 
2580
- else if( this._mustHightlightWord( token, this.builtin ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2771
+ else if( this._mustHightlightWord( token, CodeEditor.builtin ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2581
2772
  token_classname = "cm-bln";
2582
2773
 
2583
- else if( this._mustHightlightWord( token, this.statementsAndDeclarations ) )
2774
+ else if( this._mustHightlightWord( token, CodeEditor.statementsAndDeclarations ) )
2584
2775
  token_classname = "cm-std";
2585
2776
 
2586
- else if( this._mustHightlightWord( token, this.symbols ) )
2777
+ else if( this._mustHightlightWord( token, CodeEditor.symbols ) )
2587
2778
  token_classname = "cm-sym";
2588
2779
 
2589
2780
  else if( token.substr( 0, singleLineCommentToken.length ) == singleLineCommentToken )
2590
2781
  token_classname = "cm-com";
2591
2782
 
2592
- else if( this.isNumber( token ) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
2783
+ else if( this._isNumber( token ) || this._isNumber( token.replace(/[px]|[em]|%/g,'') ) )
2593
2784
  token_classname = "cm-dec";
2594
2785
 
2595
2786
  else if( this._isCSSClass( token, prev, next ) )
@@ -2601,7 +2792,7 @@ class CodeEditor {
2601
2792
  else if ( highlight == 'batch' && ( token == '@' || prev == ':' || prev == '@' ) )
2602
2793
  token_classname = "cm-kwd";
2603
2794
 
2604
- else if ( highlight == 'cpp' && token.includes( '#' ) ) // C++ preprocessor
2795
+ else if ( [ 'cpp', 'wgsl', 'glsl' ].indexOf( highlight ) > -1 && token.includes( '#' ) ) // C++ preprocessor
2605
2796
  token_classname = "cm-ppc";
2606
2797
 
2607
2798
  else if ( highlight == 'cpp' && prev == '<' && (next == '>' || next == '*') ) // Defining template type in C++
@@ -2626,9 +2817,10 @@ class CodeEditor {
2626
2817
  token_classname = "cm-mtd";
2627
2818
 
2628
2819
 
2629
- if( usesBlockComments && this._buildingBlockComment
2820
+ if( usesBlockComments && this._buildingBlockComment != undefined
2630
2821
  && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
2631
2822
  {
2823
+ this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, this._currentLineNumber ) );
2632
2824
  delete this._buildingBlockComment;
2633
2825
  }
2634
2826
 
@@ -2677,16 +2869,27 @@ class CodeEditor {
2677
2869
  _enclosedByTokens( token, tokenIndex, tagStart, tagEnd ) {
2678
2870
 
2679
2871
  const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];
2680
- const tagStartIndex = indexOfFrom(this._currentLineString, tagStart, tokenStartIndex, true );
2872
+ const tagStartIndex = indexOfFrom( this._currentLineString, tagStart, tokenStartIndex, true );
2681
2873
  if( tagStartIndex < 0 ) // Not found..
2682
2874
  return;
2683
- const tagEndIndex = indexOfFrom(this._currentLineString, tagEnd, tokenStartIndex );
2875
+ const tagEndIndex = indexOfFrom( this._currentLineString, tagEnd, tokenStartIndex );
2684
2876
  if( tagEndIndex < 0 ) // Not found..
2685
2877
  return;
2686
2878
 
2687
2879
  return ( tagStartIndex < tokenStartIndex ) && ( tagEndIndex >= ( tokenStartIndex + token.length ) );
2688
2880
  }
2689
2881
 
2882
+ _inBlockCommentSection( line ) {
2883
+
2884
+ for( var section of this._blockCommentCache )
2885
+ {
2886
+ if( line >= section.x && line <= section.y )
2887
+ return true;
2888
+ }
2889
+
2890
+ return false;
2891
+ }
2892
+
2690
2893
  _isCSSClass( token, prev, next ) {
2691
2894
 
2692
2895
  if( this.highlight != 'CSS' )
@@ -2697,20 +2900,26 @@ class CodeEditor {
2697
2900
  || ( token[ 0 ] == '#' && prev != ':' ) );
2698
2901
  }
2699
2902
 
2700
- isNumber( token ) {
2903
+ _isNumber( token ) {
2701
2904
 
2702
2905
  if(this.highlight == 'C++')
2703
2906
  {
2704
2907
  if( token.lastChar == 'f' )
2705
- return this.isNumber( token.substring(0, token.length - 1) )
2908
+ return this._isNumber( token.substring(0, token.length - 1) )
2706
2909
  else if( token.lastChar == 'u' )
2707
- return !(token.includes('.')) && this.isNumber( token.substring(0, token.length - 1) );
2910
+ return !(token.includes('.')) && this._isNumber( token.substring(0, token.length - 1) );
2911
+ }
2912
+
2913
+ else if(this.highlight == 'WGSL')
2914
+ {
2915
+ if( token.lastChar == 'u' )
2916
+ return !(token.includes('.')) && this._isNumber( token.substring(0, token.length - 1) );
2708
2917
  }
2709
2918
 
2710
2919
  else if(this.highlight == 'CSS')
2711
2920
  {
2712
2921
  if( token.lastChar == '%' )
2713
- return this.isNumber( token.substring(0, token.length - 1) )
2922
+ return this._isNumber( token.substring(0, token.length - 1) )
2714
2923
  }
2715
2924
 
2716
2925
  return token.length && token != ' ' && !Number.isNaN(+token);
@@ -2719,7 +2928,7 @@ class CodeEditor {
2719
2928
  _isType( token, prev, next ) {
2720
2929
 
2721
2930
  // Common case
2722
- if( this._mustHightlightWord( token, this.types ) )
2931
+ if( this._mustHightlightWord( token, CodeEditor.types ) )
2723
2932
  return true;
2724
2933
 
2725
2934
  if( this.highlight == 'JavaScript' )
@@ -2732,11 +2941,12 @@ class CodeEditor {
2732
2941
  }
2733
2942
  else if ( this.highlight == 'WGSL' )
2734
2943
  {
2735
- const is_kwd = !this._mustHightlightWord( token, this.keywords );
2736
- return (prev == 'struct' && next == '{') ||
2737
- ( is_kwd &&
2944
+ const not_kwd = !this._mustHightlightWord( token, CodeEditor.keywords );
2945
+ return (prev == 'struct' && next == '{') ||
2946
+ (not_kwd && prev == ':' && next == ';') ||
2947
+ ( not_kwd &&
2738
2948
  ( prev == ':' && next == ')' || prev == ':' && next == ',' || prev == '>' && next == '{'
2739
- || prev == '<' && next == ',' || prev == '<' && next == '>' || prev == '>' && !next ));
2949
+ || prev == '<' && next == ',' || prev == '<' && next == '>' || prev == '>' && token != ';' && !next ));
2740
2950
  }
2741
2951
  }
2742
2952
 
@@ -2804,6 +3014,7 @@ class CodeEditor {
2804
3014
  return false;
2805
3015
 
2806
3016
  cursor.line++;
3017
+
2807
3018
  this.cursorToBottom( cursor, resetLeft );
2808
3019
 
2809
3020
  return true;
@@ -2876,16 +3087,25 @@ class CodeEditor {
2876
3087
  delete this._lastSelectionKeyDir;
2877
3088
  }
2878
3089
 
2879
- selectAll( cursor ) {
3090
+ selectAll() {
3091
+
3092
+ // Use main cursor
3093
+ this._removeSecondaryCursors();
2880
3094
 
3095
+ var cursor = this._getCurrentCursor();
2881
3096
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP, cursor );
3097
+
2882
3098
  this.startSelection( cursor );
3099
+
2883
3100
  const nlines = this.code.lines.length - 1;
2884
3101
  this.selection.toX = this.code.lines[ nlines ].length;
2885
3102
  this.selection.toY = nlines;
3103
+
2886
3104
  this.cursorToPosition( cursor, this.selection.toX );
2887
3105
  this.cursorToLine( cursor, this.selection.toY );
3106
+
2888
3107
  this.processSelection( null, true );
3108
+
2889
3109
  this.hideAutoCompleteBox();
2890
3110
  }
2891
3111
 
@@ -2988,22 +3208,51 @@ class CodeEditor {
2988
3208
 
2989
3209
  saveCursor( cursor, state = {} ) {
2990
3210
 
2991
- state.top = cursor._top;
2992
- state.left = cursor._left;
2993
- state.line = cursor.line;
2994
3211
  state.position = cursor.position;
2995
-
3212
+ state.line = cursor.line;
2996
3213
  return state;
2997
3214
  }
2998
3215
 
3216
+ saveCursors() {
3217
+
3218
+ var cursors = [];
3219
+
3220
+ for( let cursor of this.cursors.children )
3221
+ {
3222
+ cursors.push( this.saveCursor( cursor ) );
3223
+ }
3224
+
3225
+ return cursors;
3226
+ }
3227
+
3228
+ relocateCursors() {
3229
+
3230
+ for( let cursor of this.cursors.children )
3231
+ {
3232
+ cursor._left = cursor.position * this.charWidth;
3233
+ cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3234
+ cursor._top = cursor.line * this.lineHeight;
3235
+ cursor.style.top = "calc(" + cursor._top + "px)";
3236
+ }
3237
+ }
3238
+
3239
+ mergeCursors( line ) {
3240
+
3241
+ console.assert( line >= 0 );
3242
+ const cursorsInLine = Array.from( this.cursors.children ).filter( v => v.line == line );
3243
+
3244
+ while( cursorsInLine.length > 1 )
3245
+ cursorsInLine.pop().remove();
3246
+ }
3247
+
2999
3248
  restoreCursor( cursor, state ) {
3000
3249
 
3001
- cursor.line = state.line ?? 0;
3002
3250
  cursor.position = state.position ?? 0;
3251
+ cursor.line = state.line ?? 0;
3003
3252
 
3004
- cursor._left = state.left ?? 0;
3253
+ cursor._left = cursor.position * this.charWidth;
3005
3254
  cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3006
- cursor._top = state.top ?? 0;
3255
+ cursor._top = cursor.line * this.lineHeight;
3007
3256
  cursor.style.top = "calc(" + cursor._top + "px)";
3008
3257
  }
3009
3258
 
@@ -3038,7 +3287,8 @@ class CodeEditor {
3038
3287
  for( var i = 0; i < n; ++i ) {
3039
3288
  this.root.dispatchEvent( new CustomEvent( 'keydown', { 'detail': {
3040
3289
  skip_undo: true,
3041
- key: ' '
3290
+ key: ' ',
3291
+ targetCursor: this._lastProcessedCursorIndex
3042
3292
  }}));
3043
3293
  }
3044
3294
  }
@@ -3324,11 +3574,11 @@ class CodeEditor {
3324
3574
 
3325
3575
  // Add language special keys...
3326
3576
  suggestions = suggestions.concat(
3327
- Object.keys( this.builtin[ this.highlight ] ?? {} ),
3328
- Object.keys( this.keywords[ this.highlight ] ?? {} ),
3329
- Object.keys( this.statementsAndDeclarations[ this.highlight ] ?? {} ),
3330
- Object.keys( this.types[ this.highlight ] ?? {} ),
3331
- Object.keys( this.utils[ this.highlight ] ?? {} )
3577
+ Object.keys( CodeEditor.builtin[ this.highlight ] ?? {} ),
3578
+ Object.keys( CodeEditor.keywords[ this.highlight ] ?? {} ),
3579
+ Object.keys( CodeEditor.statementsAndDeclarations[ this.highlight ] ?? {} ),
3580
+ Object.keys( CodeEditor.types[ this.highlight ] ?? {} ),
3581
+ Object.keys( CodeEditor.utils[ this.highlight ] ?? {} )
3332
3582
  );
3333
3583
 
3334
3584
  // Add words in current tab plus remove current word
@@ -3350,9 +3600,9 @@ class CodeEditor {
3350
3600
 
3351
3601
  var icon = document.createElement( 'a' );
3352
3602
 
3353
- if( this._mustHightlightWord( s, this.utils ) )
3603
+ if( this._mustHightlightWord( s, CodeEditor.utils ) )
3354
3604
  icon.className = "fa fa-cube";
3355
- else if( this._mustHightlightWord( s, this.types ) )
3605
+ else if( this._mustHightlightWord( s, CodeEditor.types ) )
3356
3606
  icon.className = "fa fa-code";
3357
3607
  else
3358
3608
  icon.className = "fa fa-font";
@@ -3600,7 +3850,7 @@ class CodeEditor {
3600
3850
 
3601
3851
  goToLine( line ) {
3602
3852
 
3603
- if( !this.isNumber( line ) )
3853
+ if( !this._isNumber( line ) )
3604
3854
  return;
3605
3855
 
3606
3856
  this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
@@ -3647,6 +3897,11 @@ class CodeEditor {
3647
3897
  }
3648
3898
  }
3649
3899
 
3900
+ _hideActiveLine() {
3901
+
3902
+ this.code.querySelectorAll( '.active-line' ).forEach( e => e.classList.remove( 'active-line' ) );
3903
+ }
3904
+
3650
3905
  _increaseFontSize() {
3651
3906
 
3652
3907
  // Change font size
@@ -3664,7 +3919,13 @@ class CodeEditor {
3664
3919
  r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
3665
3920
  this.lineHeight = row_pixels;
3666
3921
 
3667
- this.processLines(); // ... it's necessary?
3922
+ // Relocate cursors
3923
+
3924
+ this.relocateCursors();
3925
+
3926
+ // Resize the code area
3927
+
3928
+ this.processLines();
3668
3929
  }
3669
3930
 
3670
3931
  _decreaseFontSize() {
@@ -3684,12 +3945,19 @@ class CodeEditor {
3684
3945
  r.style.setProperty( "--code-editor-row-height", row_pixels + "px" );
3685
3946
  this.lineHeight = row_pixels;
3686
3947
 
3687
- this.processLines(); // ... it's necessary?
3948
+ // Relocate cursors
3949
+
3950
+ this.relocateCursors();
3951
+
3952
+ // Resize the code area
3953
+
3954
+ this.processLines();
3688
3955
  }
3689
3956
 
3690
3957
  _clearTmpVariables() {
3691
3958
 
3692
3959
  delete this._currentLineString;
3960
+ delete this._currentLineNumber;
3693
3961
  delete this._buildingString;
3694
3962
  delete this._pendingString;
3695
3963
  delete this._buildingBlockComment;
@@ -3697,6 +3965,87 @@ class CodeEditor {
3697
3965
  }
3698
3966
  }
3699
3967
 
3968
+ CodeEditor.keywords = {
3969
+
3970
+ 'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
3971
+ 'arguments', 'extends', 'instanceof'],
3972
+ 'C++': ['int', 'float', 'double', 'bool', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'nullptr',
3973
+ 'NULL', 'unsigned', 'namespace'],
3974
+ 'JSON': ['true', 'false'],
3975
+ 'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
3976
+ 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
3977
+ 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
3978
+ 'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
3979
+ 'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
3980
+ 'texture_storage_2d_array', 'texture_storage_3d', 'vec2u', 'vec3u', 'vec4u'],
3981
+ 'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
3982
+ 'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
3983
+ 'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
3984
+ 'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
3985
+ 'DRIVERQUERY', 'print', 'PRINT'],
3986
+ 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
3987
+ 'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
3988
+ };
3989
+
3990
+ CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
3991
+
3992
+ 'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
3993
+ 'bind', 'prototype', 'length', 'assign', 'entries', 'values', 'concat', 'substring', 'substr', 'splice', 'slice', 'buffer', 'appendChild', 'createElement', 'prompt',
3994
+ 'alert'],
3995
+ 'WGSL': ['textureSample'],
3996
+ 'Python': ['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
3997
+ 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance',
3998
+ 'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr',
3999
+ 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
4000
+ };
4001
+
4002
+ CodeEditor.types = {
4003
+
4004
+ 'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder'],
4005
+ 'Rust': ['u128'],
4006
+ 'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
4007
+ 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
4008
+ 'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
4009
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
4010
+ 'C++': ['uint8_t', 'uint16_t', 'uint32_t']
4011
+ };
4012
+
4013
+ CodeEditor.builtin = {
4014
+
4015
+ 'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
4016
+ 'CSS': ['*', '!important'],
4017
+ 'C++': ['vector', 'list', 'map'],
4018
+ 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
4019
+ 'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
4020
+ };
4021
+
4022
+ CodeEditor.statementsAndDeclarations = {
4023
+
4024
+ 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
4025
+ 'CSS': ['@', 'import'],
4026
+ 'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog'],
4027
+ 'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
4028
+ 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'read_write', 'uniform', 'function', 'workgroup'],
4029
+ 'Rust': ['break', 'else', 'continue', 'for', 'if', 'loop', 'match', 'return', 'while', 'do', 'yield'],
4030
+ 'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue',
4031
+ 'global', 'pass'],
4032
+ 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT']
4033
+ };
4034
+
4035
+ CodeEditor.symbols = {
4036
+
4037
+ 'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
4038
+ 'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
4039
+ 'JSON': ['[', ']', '{', '}', '(', ')'],
4040
+ 'GLSL': ['[', ']', '{', '}', '(', ')'],
4041
+ 'WGSL': ['[', ']', '{', '}', '(', ')', '->'],
4042
+ 'CSS': ['{', '}', '(', ')', '*'],
4043
+ 'Rust': ['<', '>', '[', ']', '(', ')', '='],
4044
+ 'Python': ['<', '>', '[', ']', '(', ')', '='],
4045
+ 'Batch': ['[', ']', '(', ')', '%'],
4046
+ 'HTML': ['<', '>', '/']
4047
+ };
4048
+
3700
4049
  LX.CodeEditor = CodeEditor;
3701
4050
 
3702
4051
  export { CodeEditor };