lexgui 0.1.24 → 0.1.26

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,12 @@ 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;
31
+ }
32
+
33
+ function strReverse( str ) {
34
+ return str.split( "" ).reverse().join( "" );
30
35
  }
31
36
 
32
37
  function indexOfFrom( str, reg, from, reverse ) {
@@ -63,16 +68,17 @@ function doAsync( fn, ms ) {
63
68
 
64
69
  class CodeSelection {
65
70
 
66
- constructor( editor, ix, iy, className = "lexcodeselection" ) {
71
+ constructor( editor, cursor, className = "lexcodeselection" ) {
67
72
 
68
73
  this.editor = editor;
69
- this.chars = 0;
74
+ this.cursor = cursor;
70
75
  this.className = className;
76
+ this.chars = 0;
71
77
 
72
- this.fromX = ix;
73
- this.toX = ix;
74
- this.fromY = iy;
75
- this.toY = iy;
78
+ this.fromX = cursor.position;
79
+ this.toX = cursor.position;
80
+ this.fromY = cursor.line;
81
+ this.toY = cursor.line;
76
82
  }
77
83
 
78
84
  sameLine() {
@@ -100,7 +106,7 @@ class CodeSelection {
100
106
  }
101
107
  }
102
108
 
103
- selectInline( x, y, width ) {
109
+ selectInline( cursor, x, y, width, isSearchResult ) {
104
110
 
105
111
  this.chars = width / this.editor.charWidth;
106
112
  this.fromX = x;
@@ -115,10 +121,44 @@ class CodeSelection {
115
121
  domEl._left = x * this.editor.charWidth;
116
122
  domEl.style.left = "calc(" + domEl._left + "px + " + this.editor.xPadding + ")";
117
123
  domEl.style.width = width + "px";
118
- this.editor.selections.appendChild(domEl);
124
+
125
+ if( isSearchResult )
126
+ {
127
+ this.editor.searchResultSelections.appendChild( domEl );
128
+ }
129
+ else
130
+ {
131
+ this.editor.selections[ cursor.name ].appendChild( domEl );
132
+ }
119
133
 
120
134
  // Hide active line background
121
- this.editor.code.childNodes.forEach( e => e.classList.remove( 'active-line' ) );
135
+ this.editor._hideActiveLine();
136
+ }
137
+
138
+ save() {
139
+
140
+ return {
141
+ fromX: this.fromX,
142
+ fromY: this.fromY,
143
+ toX: this.toX,
144
+ toY: this.toY
145
+ }
146
+ }
147
+
148
+ load( data ) {
149
+
150
+ this.fromX = data.fromX;
151
+ this.fromY = data.fromY;
152
+ this.toX = data.toX;
153
+ this.toY = data.toY;
154
+ }
155
+
156
+ getText() {
157
+
158
+ if( !this.editor.code || !this.sameLine() )
159
+ return null;
160
+
161
+ return this.editor.code.lines[ this.fromY ].substring( this.fromX, this.toX );
122
162
  }
123
163
  };
124
164
 
@@ -140,6 +180,7 @@ class ScrollBar {
140
180
  this.thumb = document.createElement( 'div' );
141
181
  this.thumb._top = 0;
142
182
  this.thumb._left = 0;
183
+
143
184
  this.root.appendChild( this.thumb );
144
185
 
145
186
  this.thumb.addEventListener( "mousedown", inner_mousedown );
@@ -151,8 +192,8 @@ class ScrollBar {
151
192
  function inner_mousedown( e )
152
193
  {
153
194
  var doc = editor.root.ownerDocument;
154
- doc.addEventListener( "mousemove",inner_mousemove );
155
- doc.addEventListener( "mouseup",inner_mouseup );
195
+ doc.addEventListener( "mousemove", inner_mousemove );
196
+ doc.addEventListener( "mouseup", inner_mouseup );
156
197
  that.lastPosition.set( e.x, e.y );
157
198
  e.stopPropagation();
158
199
  e.preventDefault();
@@ -201,8 +242,8 @@ class CodeEditor {
201
242
  static WORD_TYPE_METHOD = 0;
202
243
  static WORD_TYPE_CLASS = 1;
203
244
 
204
- static CODE_MAX_FONT_SIZE = 22;
205
245
  static CODE_MIN_FONT_SIZE = 9;
246
+ static CODE_MAX_FONT_SIZE = 22;
206
247
 
207
248
  /**
208
249
  * @param {*} options
@@ -324,9 +365,13 @@ class CodeEditor {
324
365
  this.cursors.className = 'cursors';
325
366
  this.tabs.area.attach( this.cursors );
326
367
 
327
- this.selections = document.createElement( 'div' );
328
- this.selections.className = 'selections';
329
- this.tabs.area.attach( this.selections );
368
+ this.searchResultSelections = document.createElement( 'div' );
369
+ this.searchResultSelections.id = 'search-selections';
370
+ this.searchResultSelections.className = 'selections';
371
+ this.tabs.area.attach( this.searchResultSelections );
372
+
373
+ // Store here selections per cursor
374
+ this.selections = {};
330
375
 
331
376
  // Css char synchronization
332
377
  this.xPadding = "48px";
@@ -493,7 +538,8 @@ class CodeEditor {
493
538
  this.state = {
494
539
  focused: false,
495
540
  selectingText: false,
496
- activeLine: null
541
+ activeLine: null,
542
+ keyChain: null
497
543
  }
498
544
 
499
545
  // Code
@@ -550,90 +596,26 @@ class CodeEditor {
550
596
  'End', 'Tab', 'Escape'
551
597
  ];
552
598
 
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
- };
599
+ this._blockCommentCache = [];
622
600
 
623
601
  // Convert reserved word arrays to maps so we can search tokens faster
624
602
 
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}), {});
603
+ for( let lang in CodeEditor.keywords ) CodeEditor.keywords[lang] = CodeEditor.keywords[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
604
+ for( let lang in CodeEditor.utils ) CodeEditor.utils[lang] = CodeEditor.utils[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
605
+ for( let lang in CodeEditor.types ) CodeEditor.types[lang] = CodeEditor.types[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
606
+ for( let lang in CodeEditor.builtin ) CodeEditor.builtin[lang] = CodeEditor.builtin[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
607
+ for( let lang in CodeEditor.statementsAndDeclarations ) CodeEditor.statementsAndDeclarations[lang] = CodeEditor.statementsAndDeclarations[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
608
+ for( let lang in CodeEditor.symbols ) CodeEditor.symbols[lang] = CodeEditor.symbols[lang].reduce((a, v) => ({ ...a, [v]: true}), {});
631
609
 
632
610
  // Action keys
633
611
 
634
612
  this.action( 'Escape', false, ( ln, cursor, e ) => {
635
- this.hideAutoCompleteBox();
636
- this.hideSearchBox();
613
+ if( this.hideAutoCompleteBox() )
614
+ return;
615
+ if( this.hideSearchBox() )
616
+ return;
617
+ // Remove selections and cursors
618
+ this.endSelection();
637
619
  this._removeSecondaryCursors();
638
620
  });
639
621
 
@@ -641,7 +623,7 @@ class CodeEditor {
641
623
 
642
624
  this._addUndoStep( cursor );
643
625
 
644
- if( this.selection ) {
626
+ if( cursor.selection ) {
645
627
  this.deleteSelection( cursor );
646
628
  // Remove entire line when selecting with triple click
647
629
  if( this._tripleClickSelection )
@@ -695,7 +677,7 @@ class CodeEditor {
695
677
 
696
678
  this._addUndoStep( cursor );
697
679
 
698
- if(this.selection) {
680
+ if( cursor.selection ) {
699
681
  // Use 'Backspace' as it's the same callback...
700
682
  this.actions['Backspace'].callback( ln, cursor, e );
701
683
  }
@@ -706,7 +688,7 @@ class CodeEditor {
706
688
  this.code.lines[ ln ] = sliceChars( this.code.lines[ ln ], cursor.position );
707
689
  this.processLine( ln );
708
690
  }
709
- else if(this.code.lines[ ln + 1 ] != undefined) {
691
+ else if( this.code.lines[ ln + 1 ] != undefined ) {
710
692
  this.code.lines[ ln ] += this.code.lines[ ln + 1 ];
711
693
  this.code.lines.splice( ln + 1, 1 );
712
694
  this.processLines();
@@ -745,18 +727,18 @@ class CodeEditor {
745
727
  if( e.shiftKey && !e.cancelShift )
746
728
  {
747
729
  // Get last selection range
748
- if( this.selection )
749
- lastX += this.selection.chars;
730
+ if( cursor.selection )
731
+ lastX += cursor.selection.chars;
750
732
 
751
- if( !this.selection )
733
+ if( !cursor.selection )
752
734
  this.startSelection( cursor );
753
735
 
754
736
  var string = this.code.lines[ ln ].substring( idx, lastX );
755
- if( this.selection.sameLine() )
756
- this.selection.selectInline( idx, cursor.line, this.measureString( string ) );
737
+ if( cursor.selection.sameLine() )
738
+ cursor.selection.selectInline( cursor, idx, cursor.line, this.measureString( string ) );
757
739
  else
758
740
  {
759
- this.processSelection( e );
741
+ this._processSelection( cursor, e );
760
742
  }
761
743
  } else if( !e.keepSelection )
762
744
  this.endSelection();
@@ -767,15 +749,15 @@ class CodeEditor {
767
749
  if( ( e.shiftKey || e._shiftKey ) && !e.cancelShift ) {
768
750
 
769
751
  var string = this.code.lines[ ln ].substring( cursor.position );
770
- if( !this.selection )
752
+ if( !cursor.selection )
771
753
  this.startSelection( cursor );
772
- if( this.selection.sameLine() )
773
- this.selection.selectInline(cursor.position, cursor.line, this.measureString( string ));
754
+ if( cursor.selection.sameLine() )
755
+ cursor.selection.selectInline( cursor, cursor.position, cursor.line, this.measureString( string ));
774
756
  else
775
757
  {
776
758
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
777
759
  this.cursorToString( cursor, this.code.lines[ ln ] );
778
- this.processSelection( e );
760
+ this._processSelection( cursor, e );
779
761
  }
780
762
  } else if( !e.keepSelection )
781
763
  this.endSelection();
@@ -837,7 +819,7 @@ class CodeEditor {
837
819
  if( !this.isAutoCompleteActive )
838
820
  {
839
821
  if( e.shiftKey ) {
840
- if( !this.selection )
822
+ if( !cursor.selection )
841
823
  this.startSelection( cursor );
842
824
 
843
825
  this.lineUp( cursor );
@@ -847,7 +829,7 @@ class CodeEditor {
847
829
  this.cursorToPosition( cursor, this.code.lines[ cursor.line ].length );
848
830
  }
849
831
 
850
- this.processSelection( e, false );
832
+ this._processSelection( cursor, e, false );
851
833
 
852
834
  } else {
853
835
  this.endSelection();
@@ -870,7 +852,7 @@ class CodeEditor {
870
852
  if( !this.isAutoCompleteActive )
871
853
  {
872
854
  if( e.shiftKey ) {
873
- if( !this.selection )
855
+ if( !cursor.selection )
874
856
  this.startSelection( cursor );
875
857
  } else {
876
858
  this.endSelection();
@@ -881,11 +863,11 @@ class CodeEditor {
881
863
 
882
864
  // Go to end of line if out of range
883
865
  if( !letter || !canGoDown ) {
884
- this.actions[ 'End' ].callback( cursor.line, cursor, e );
866
+ this.cursorToPosition( cursor, Math.max(this.code.lines[ cursor.line ].length, 0) );
885
867
  }
886
868
 
887
869
  if( e.shiftKey ) {
888
- this.processSelection( e );
870
+ this._processSelection( cursor, e );
889
871
  }
890
872
  }
891
873
  // Move down autocomplete selection
@@ -919,37 +901,37 @@ class CodeEditor {
919
901
  var diff = Math.max( cursor.position - from, 1 );
920
902
  var substr = word.substr( 0, diff );
921
903
  // Selections...
922
- if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
904
+ if( e.shiftKey ) { if( !cursor.selection ) this.startSelection( cursor ); }
923
905
  else this.endSelection();
924
906
  this.cursorToString( cursor, substr, true );
925
- if( e.shiftKey ) this.processSelection( e, false, true );
907
+ if( e.shiftKey ) this._processSelection( cursor, e, false, true );
926
908
  }
927
909
  else {
928
910
  var letter = this.getCharAtPos( cursor, -1 );
929
911
  if( letter ) {
930
912
  if( e.shiftKey ) {
931
- if( !this.selection ) this.startSelection( cursor );
913
+ if( !cursor.selection ) this.startSelection( cursor );
932
914
  this.cursorToLeft( letter, cursor );
933
- this.processSelection( e, false, CodeEditor.SELECTION_X );
915
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
934
916
  }
935
917
  else {
936
- if( !this.selection ) {
918
+ if( !cursor.selection ) {
937
919
  this.cursorToLeft( letter, cursor );
938
920
  if( this.useAutoComplete && this.isAutoCompleteActive )
939
921
  this.showAutoCompleteBox( 'foo', cursor );
940
922
  }
941
923
  else {
942
- this.selection.invertIfNecessary();
924
+ cursor.selection.invertIfNecessary();
943
925
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP, cursor );
944
- this.cursorToLine( cursor, this.selection.fromY, true );
945
- this.cursorToPosition( cursor, this.selection.fromX );
926
+ this.cursorToLine( cursor, cursor.selection.fromY, true );
927
+ this.cursorToPosition( cursor, cursor.selection.fromX );
946
928
  this.endSelection();
947
929
  }
948
930
  }
949
931
  }
950
932
  else if( cursor.line > 0 ) {
951
933
 
952
- if( e.shiftKey && !this.selection ) this.startSelection( cursor );
934
+ if( e.shiftKey && !cursor.selection ) this.startSelection( cursor );
953
935
 
954
936
  this.lineUp( cursor );
955
937
 
@@ -957,7 +939,7 @@ class CodeEditor {
957
939
  this.actions[ 'End' ].callback( cursor.line, cursor, e );
958
940
  delete e.cancelShift; delete e.keepSelection;
959
941
 
960
- if( e.shiftKey ) this.processSelection( e, false );
942
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
961
943
  }
962
944
  }
963
945
  });
@@ -969,40 +951,61 @@ class CodeEditor {
969
951
  cursor.position == this.code.lines[ cursor.line ].length )
970
952
  return;
971
953
 
972
- if( e.metaKey ) { // Apple devices (Command)
954
+ if( e.metaKey ) // Apple devices (Command)
955
+ {
973
956
  e.preventDefault();
974
957
  this.actions[ 'End' ].callback( ln, cursor );
975
- } else if( e.ctrlKey ) {
958
+ }
959
+ else if( e.ctrlKey ) // Next word
960
+ {
976
961
  // Get next word
977
962
  const [ word, from, to ] = this.getWordAtPos( cursor );
963
+
978
964
  // If no length, we change line..
979
965
  if( !word.length ) this.lineDown( cursor, true );
980
966
  var diff = cursor.position - from;
981
967
  var substr = word.substr( diff );
968
+
982
969
  // Selections...
983
- if( e.shiftKey ) { if( !this.selection ) this.startSelection( cursor ); }
984
- else this.endSelection();
970
+ if( e.shiftKey ) {
971
+ if( !cursor.selection )
972
+ this.startSelection( cursor );
973
+ }
974
+ else
975
+ this.endSelection();
976
+
985
977
  this.cursorToString( cursor, substr );
986
- if( e.shiftKey ) this.processSelection( e );
987
- } else {
978
+
979
+ if( e.shiftKey )
980
+ this._processSelection( cursor, e );
981
+ }
982
+ else // Next char
983
+ {
988
984
  var letter = this.getCharAtPos( cursor );
989
985
  if( letter ) {
990
- if( e.shiftKey ) {
991
- if( !this.selection ) this.startSelection( cursor );
986
+
987
+ // Selecting chars
988
+ if( e.shiftKey )
989
+ {
990
+ if( !cursor.selection )
991
+ this.startSelection( cursor );
992
+
992
993
  this.cursorToRight( letter, cursor );
993
- this.processSelection( e, false, CodeEditor.SELECTION_X );
994
- }else{
995
- if( !this.selection ) {
994
+ this._processSelection( cursor, e, false, CodeEditor.SELECTION_X );
995
+ }
996
+ else
997
+ {
998
+ if( !cursor.selection ) {
996
999
  this.cursorToRight( letter, cursor );
997
1000
  if( this.useAutoComplete && this.isAutoCompleteActive )
998
1001
  this.showAutoCompleteBox( 'foo', cursor );
999
1002
  }
1000
- else
1003
+ else
1001
1004
  {
1002
- this.selection.invertIfNecessary();
1005
+ cursor.selection.invertIfNecessary();
1003
1006
  this.resetCursorPos( CodeEditor.CURSOR_LEFT | CodeEditor.CURSOR_TOP, cursor );
1004
- this.cursorToLine( cursor, this.selection.toY );
1005
- this.cursorToPosition( cursor, this.selection.toX );
1007
+ this.cursorToLine( cursor, cursor.selection.toY );
1008
+ this.cursorToPosition( cursor, cursor.selection.toX );
1006
1009
  this.endSelection();
1007
1010
  }
1008
1011
  }
@@ -1010,12 +1013,13 @@ class CodeEditor {
1010
1013
  else if( this.code.lines[ cursor.line + 1 ] !== undefined ) {
1011
1014
 
1012
1015
  if( e.shiftKey ) {
1013
- if( !this.selection ) this.startSelection( cursor );
1016
+ if( !cursor.selection ) this.startSelection( cursor );
1014
1017
  }
1018
+ else this.endSelection();
1015
1019
 
1016
1020
  this.lineDown( cursor, true );
1017
1021
 
1018
- if( e.shiftKey ) this.processSelection( e, false );
1022
+ if( e.shiftKey ) this._processSelection( cursor, e, false );
1019
1023
 
1020
1024
  this.hideAutoCompleteBox();
1021
1025
  }
@@ -1115,14 +1119,14 @@ class CodeEditor {
1115
1119
 
1116
1120
  let lidx = cursor.line;
1117
1121
 
1118
- if( this.selection ) {
1122
+ if( cursor.selection ) {
1119
1123
  this.deleteSelection( cursor );
1120
1124
  lidx = cursor.line;
1121
1125
  }
1122
1126
 
1123
1127
  this.endSelection();
1124
1128
 
1125
- const new_lines = text.split( '\n' );
1129
+ const new_lines = text.replaceAll( '\r', '' ).split( '\n' );
1126
1130
 
1127
1131
  // Pasting Multiline...
1128
1132
  if( new_lines.length != 1 )
@@ -1177,10 +1181,16 @@ class CodeEditor {
1177
1181
 
1178
1182
  const inner_add_tab = ( text, name, title ) => {
1179
1183
 
1184
+ // Remove Carriage Return in some cases and sub tabs using spaces
1185
+ text = text.replaceAll( '\r', '' );
1186
+ text = text.replaceAll( /\t|\\t/g, ' '.repeat( this.tabSpaces ) );
1187
+
1180
1188
  // Set current text and language
1181
- const lines = text.replaceAll( '\r', '' ).split( '\n' );
1189
+
1190
+ const lines = text.split( '\n' );
1182
1191
 
1183
1192
  // Add item in the explorer if used
1193
+
1184
1194
  if( this.explorer )
1185
1195
  {
1186
1196
  this._storedLines = this._storedLines ?? {};
@@ -1227,8 +1237,9 @@ class CodeEditor {
1227
1237
 
1228
1238
  return;
1229
1239
  }
1230
-
1240
+
1231
1241
  let cursor = document.createElement( 'div' );
1242
+ cursor.name = "cursor" + this.cursors.childElementCount;
1232
1243
  cursor.className = "cursor";
1233
1244
  cursor.innerHTML = "&nbsp;";
1234
1245
  cursor.isMain = isMain;
@@ -1348,6 +1359,8 @@ class CodeEditor {
1348
1359
 
1349
1360
  this.restoreCursor( currentCursor, step.cursors[ i ] );
1350
1361
  }
1362
+
1363
+ this._hideActiveLine();
1351
1364
  }
1352
1365
 
1353
1366
  _addRedoStep( cursor ) {
@@ -1703,12 +1716,12 @@ class CodeEditor {
1703
1716
  {
1704
1717
  this.processClick( e );
1705
1718
 
1706
- this.canOpenContextMenu = !this.selection;
1719
+ this.canOpenContextMenu = !cursor.selection;
1707
1720
 
1708
- if( this.selection )
1721
+ if( cursor.selection )
1709
1722
  {
1710
- this.canOpenContextMenu |= (cursor.line >= this.selection.fromY && cursor.line <= this.selection.toY
1711
- && cursor.position >= this.selection.fromX && cursor.position <= this.selection.toX);
1723
+ this.canOpenContextMenu |= (cursor.line >= cursor.selection.fromY && cursor.line <= cursor.selection.toY
1724
+ && cursor.position >= cursor.selection.fromX && cursor.position <= cursor.selection.toX);
1712
1725
  if( this.canOpenContextMenu )
1713
1726
  return;
1714
1727
  }
@@ -1728,7 +1741,7 @@ class CodeEditor {
1728
1741
  else if( e.type == 'mousemove' )
1729
1742
  {
1730
1743
  if( this.state.selectingText )
1731
- this.processSelection( e );
1744
+ this.processSelections( e );
1732
1745
  }
1733
1746
 
1734
1747
  else if ( e.type == 'click' ) // trip
@@ -1740,7 +1753,7 @@ class CodeEditor {
1740
1753
  this.resetCursorPos( CodeEditor.CURSOR_LEFT, cursor );
1741
1754
  this.cursorToPosition( cursor, from );
1742
1755
  this.startSelection( cursor );
1743
- this.selection.selectInline( from, cursor.line, this.measureString( word ) );
1756
+ cursor.selection.selectInline( cursor, from, cursor.line, this.measureString( word ) );
1744
1757
  this.cursorToString( cursor, word ); // Go to the end of the word
1745
1758
  break;
1746
1759
  // Select entire line
@@ -1769,6 +1782,8 @@ class CodeEditor {
1769
1782
  m.add( "" );
1770
1783
  m.add( "Format/JSON", () => {
1771
1784
  let json = this.toJSONFormat( this.getText() );
1785
+ if( !json )
1786
+ return;
1772
1787
  this.code.lines = json.split( "\n" );
1773
1788
  this.processLines();
1774
1789
  } );
@@ -1786,10 +1801,9 @@ class CodeEditor {
1786
1801
  this.endSelection();
1787
1802
  }
1788
1803
 
1789
- if( this.selection )
1790
- {
1791
- this.selection.invertIfNecessary();
1792
- }
1804
+ const cursor = this._getCurrentCursor();
1805
+ if( cursor.selection )
1806
+ cursor.selection.invertIfNecessary();
1793
1807
 
1794
1808
  this.state.selectingText = false;
1795
1809
  delete this._lastSelectionKeyDir;
@@ -1828,17 +1842,35 @@ class CodeEditor {
1828
1842
  this.hideAutoCompleteBox();
1829
1843
  }
1830
1844
 
1831
- processSelection( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
1845
+ updateSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
1846
+
1847
+ for( let cursor of this.cursors.children )
1848
+ {
1849
+ if( !cursor.selection )
1850
+ continue;
1851
+
1852
+ this._processSelection( cursor, e, keep_range, flags );
1853
+ }
1854
+ }
1855
+
1856
+ processSelections( e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
1857
+
1858
+ for( let cursor of this.cursors.children )
1859
+ {
1860
+ this._processSelection( cursor, e, keep_range, flags );
1861
+ }
1862
+ }
1863
+
1864
+ _processSelection( cursor, e, keep_range, flags = CodeEditor.SELECTION_X_Y ) {
1832
1865
 
1833
- var cursor = this._getCurrentCursor();
1834
1866
  const isMouseEvent = e && ( e.constructor == MouseEvent );
1835
1867
 
1836
1868
  if( isMouseEvent ) this.processClick( e );
1837
- if( !this.selection )
1869
+
1870
+ if( !cursor.selection )
1838
1871
  this.startSelection( cursor );
1839
1872
 
1840
- // Hide active line background
1841
- this.code.childNodes.forEach( e => e.classList.remove( 'active-line' ) );
1873
+ this._hideActiveLine();
1842
1874
 
1843
1875
  // Update selection
1844
1876
  if( !keep_range )
@@ -1846,46 +1878,48 @@ class CodeEditor {
1846
1878
  let ccw = true;
1847
1879
 
1848
1880
  // Check if we must change ccw or not ... (not with mouse)
1849
- if( !isMouseEvent && this.line >= this.selection.fromY &&
1850
- (this.line == this.selection.fromY ? this.position >= this.selection.fromX : true) )
1881
+ if( !isMouseEvent && this.line >= cursor.selection.fromY &&
1882
+ (this.line == cursor.selection.fromY ? this.position >= cursor.selection.fromX : true) )
1851
1883
  {
1852
1884
  ccw = ( e && this._lastSelectionKeyDir && ( e.key == 'ArrowRight' || e.key == 'ArrowDown' || e.key == 'End' ) );
1853
1885
  }
1854
1886
 
1855
1887
  if( ccw )
1856
1888
  {
1857
- if( flags & CodeEditor.SELECTION_X ) this.selection.fromX = cursor.position;
1858
- if( flags & CodeEditor.SELECTION_Y ) this.selection.fromY = cursor.line;
1889
+ if( flags & CodeEditor.SELECTION_X ) cursor.selection.fromX = cursor.position;
1890
+ if( flags & CodeEditor.SELECTION_Y ) cursor.selection.fromY = cursor.line;
1859
1891
  }
1860
1892
  else
1861
1893
  {
1862
- if( flags & CodeEditor.SELECTION_X ) this.selection.toX = cursor.position;
1863
- if( flags & CodeEditor.SELECTION_Y ) this.selection.toY = cursor.line;
1894
+ if( flags & CodeEditor.SELECTION_X ) cursor.selection.toX = cursor.position;
1895
+ if( flags & CodeEditor.SELECTION_Y ) cursor.selection.toY = cursor.line;
1864
1896
  }
1865
1897
 
1866
1898
  this._lastSelectionKeyDir = ccw;
1867
1899
  }
1868
1900
 
1869
1901
  // Only leave if not a mouse selection...
1870
- if( !isMouseEvent && this.selection.isEmpty() )
1902
+ if( !isMouseEvent && cursor.selection.isEmpty() )
1871
1903
  {
1872
1904
  this.endSelection();
1873
1905
  return;
1874
1906
  }
1875
1907
 
1876
- this.selection.chars = 0;
1908
+ cursor.selection.chars = 0;
1877
1909
 
1878
- const fromX = this.selection.fromX,
1879
- fromY = this.selection.fromY,
1880
- toX = this.selection.toX,
1881
- toY = this.selection.toY;
1910
+ const fromX = cursor.selection.fromX,
1911
+ fromY = cursor.selection.fromY,
1912
+ toX = cursor.selection.toX,
1913
+ toY = cursor.selection.toY;
1882
1914
  const deltaY = toY - fromY;
1883
1915
 
1916
+ let cursorSelections = this.selections[ cursor.name ];
1917
+
1884
1918
  // Selection goes down...
1885
1919
  if( deltaY >= 0 )
1886
1920
  {
1887
- while( deltaY < ( this.selections.childElementCount - 1 ) )
1888
- deleteElement( this.selections.lastChild );
1921
+ while( deltaY < ( cursorSelections.childElementCount - 1 ) )
1922
+ deleteElement( cursorSelections.lastChild );
1889
1923
 
1890
1924
  for(let i = fromY; i <= toY; i++){
1891
1925
 
@@ -1896,12 +1930,12 @@ class CodeEditor {
1896
1930
  if( isVisible )
1897
1931
  {
1898
1932
  // Make sure that the line selection is generated...
1899
- domEl = this.selections.childNodes[ sId ];
1933
+ domEl = cursorSelections.childNodes[ sId ];
1900
1934
  if(!domEl)
1901
1935
  {
1902
1936
  domEl = document.createElement( 'div' );
1903
1937
  domEl.className = "lexcodeselection";
1904
- this.selections.appendChild( domEl );
1938
+ cursorSelections.appendChild( domEl );
1905
1939
  }
1906
1940
  }
1907
1941
 
@@ -1923,7 +1957,7 @@ class CodeEditor {
1923
1957
  }
1924
1958
 
1925
1959
  const stringWidth = this.measureString( string );
1926
- this.selection.chars += stringWidth / this.charWidth;
1960
+ cursor.selection.chars += stringWidth / this.charWidth;
1927
1961
 
1928
1962
  if( isVisible )
1929
1963
  {
@@ -1935,8 +1969,8 @@ class CodeEditor {
1935
1969
  }
1936
1970
  else // Selection goes up...
1937
1971
  {
1938
- while( Math.abs( deltaY ) < ( this.selections.childElementCount - 1 ) )
1939
- deleteElement( this.selections.firstChild );
1972
+ while( Math.abs( deltaY ) < ( cursorSelections.childElementCount - 1 ) )
1973
+ deleteElement( cursorSelections.firstChild );
1940
1974
 
1941
1975
  for( let i = toY; i <= fromY; i++ ){
1942
1976
 
@@ -1947,12 +1981,12 @@ class CodeEditor {
1947
1981
  if( isVisible )
1948
1982
  {
1949
1983
  // Make sure that the line selection is generated...
1950
- domEl = this.selections.childNodes[ sId ];
1984
+ domEl = cursorSelections.childNodes[ sId ];
1951
1985
  if(!domEl)
1952
1986
  {
1953
1987
  domEl = document.createElement( 'div' );
1954
1988
  domEl.className = "lexcodeselection";
1955
- this.selections.appendChild( domEl );
1989
+ cursorSelections.appendChild( domEl );
1956
1990
  }
1957
1991
  }
1958
1992
 
@@ -1972,7 +2006,7 @@ class CodeEditor {
1972
2006
  }
1973
2007
 
1974
2008
  const stringWidth = this.measureString( string );
1975
- this.selection.chars += stringWidth / this.charWidth;
2009
+ cursor.selection.chars += stringWidth / this.charWidth;
1976
2010
 
1977
2011
  if( isVisible )
1978
2012
  {
@@ -1994,14 +2028,21 @@ class CodeEditor {
1994
2028
  const key = e.key ?? e.detail.key;
1995
2029
  const target = e.detail.targetCursor;
1996
2030
 
2031
+ // Global keys
2032
+
2033
+ if( this._processGlobalKeys( e, key ) ) {
2034
+ // Stop propagation..
2035
+ return;
2036
+ }
2037
+
2038
+ // By cursor keys
2039
+
1997
2040
  if( target !== undefined )
1998
2041
  {
1999
2042
  this.processKeyAtTargetCursor( e, key, target );
2000
2043
  return;
2001
2044
  }
2002
2045
 
2003
- // By cursor keys
2004
-
2005
2046
  this._lastProcessedCursorIndex = null;
2006
2047
 
2007
2048
  var lastProcessedCursor = null;
@@ -2033,10 +2074,6 @@ class CodeEditor {
2033
2074
  cursorOffset.y += ( cursor.line - lastProcessedCursor.line );
2034
2075
  }
2035
2076
 
2036
- // Global keys
2037
-
2038
- this._processGlobalKeys( e, key );
2039
-
2040
2077
  // Clear tmp
2041
2078
 
2042
2079
  delete this._lastProcessedCursorIndex;
@@ -2054,41 +2091,72 @@ class CodeEditor {
2054
2091
  this._processGlobalKeys( e, key );
2055
2092
  }
2056
2093
 
2057
- async _processGlobalKeys( e, key ) {
2094
+ _processGlobalKeys( e, key ) {
2058
2095
 
2059
2096
  let cursor = this._getCurrentCursor();
2060
2097
 
2061
2098
  if( e.ctrlKey || e.metaKey )
2062
2099
  {
2063
- e.preventDefault();
2064
-
2065
2100
  switch( key.toLowerCase() ) {
2066
2101
  case 'a': // select all
2102
+ e.preventDefault();
2067
2103
  this.selectAll();
2068
- break;
2104
+ return true;
2105
+ case 'c': // k+c, comment line
2106
+ e.preventDefault();
2107
+ if( this.state.keyChain == 'k' ) {
2108
+ this._commentLines();
2109
+ return true;
2110
+ }
2111
+ return false;
2112
+ case 'd': // next ocurrence
2113
+ e.preventDefault();
2114
+ this.selectNextOcurrence( cursor );
2115
+ return true;
2069
2116
  case 'f': // find/search
2117
+ e.preventDefault();
2070
2118
  this.showSearchBox();
2071
- break;
2119
+ return true;
2072
2120
  case 'g': // find line
2121
+ e.preventDefault();
2073
2122
  this.showSearchLineBox();
2074
- break;
2123
+ return true;
2124
+ case 'k': // shortcut chain
2125
+ e.preventDefault();
2126
+ this.state.keyChain = 'k';
2127
+ return true;
2075
2128
  case 's': // save
2129
+ e.preventDefault();
2076
2130
  this.onsave( this.getText() );
2077
- break;
2131
+ return true;
2132
+ case 'u': // k+u, uncomment line
2133
+ e.preventDefault();
2134
+ if( this.state.keyChain == 'k' ) {
2135
+ this._uncommentLines();
2136
+ return true;
2137
+ }
2138
+ return false;
2078
2139
  case 'y': // redo
2140
+ e.preventDefault();
2079
2141
  this._doRedo( cursor );
2080
- break;
2142
+ return true;
2081
2143
  case 'z': // undo
2144
+ e.preventDefault();
2082
2145
  this._doUndo( cursor );
2083
- break;
2146
+ return true;
2084
2147
  case '+': // increase size
2148
+ e.preventDefault();
2085
2149
  this._increaseFontSize();
2086
- break;
2150
+ return true;
2087
2151
  case '-': // decrease size
2152
+ e.preventDefault();
2088
2153
  this._decreaseFontSize();
2089
- break;
2154
+ return true;
2090
2155
  }
2091
2156
  }
2157
+
2158
+ this.state.keyChain = null;
2159
+ return false;
2092
2160
  }
2093
2161
 
2094
2162
  async _processKeyAtCursor( e, key, cursor ) {
@@ -2110,14 +2178,8 @@ class CodeEditor {
2110
2178
  {
2111
2179
  switch( key.toLowerCase() ) {
2112
2180
  case 'c': // copy
2113
- // TODO: COPY TEXT FROM EVERY CURSOR
2114
2181
  this._copyContent( cursor );
2115
2182
  return;
2116
- case 'd': // duplicate line
2117
- e.preventDefault();
2118
- // TODO: UPDATE NEXT CURSOR ON MODIFY STATE
2119
- this._duplicateLine( lidx, cursor );
2120
- return;
2121
2183
  case 'v': // paste
2122
2184
  this._pasteContent( cursor );
2123
2185
  return;
@@ -2138,6 +2200,10 @@ class CodeEditor {
2138
2200
  else if( e.altKey )
2139
2201
  {
2140
2202
  switch( key ) {
2203
+ case 'd': // duplicate line
2204
+ e.preventDefault();
2205
+ this._duplicateLine( lidx, cursor );
2206
+ return;
2141
2207
  case 'ArrowUp':
2142
2208
  if(this.code.lines[ lidx - 1 ] == undefined)
2143
2209
  return;
@@ -2168,7 +2234,7 @@ class CodeEditor {
2168
2234
  if( key != actKey ) continue;
2169
2235
  e.preventDefault();
2170
2236
 
2171
- if( this.actions[ key ].deleteSelection && this.selection )
2237
+ if( this.actions[ key ].deleteSelection && cursor.selection )
2172
2238
  this.actions['Backspace'].callback( lidx, cursor, e );
2173
2239
 
2174
2240
  return this.actions[ key ].callback( lidx, cursor, e );
@@ -2197,7 +2263,7 @@ class CodeEditor {
2197
2263
  // Until this point, if there was a selection, we need
2198
2264
  // to delete the content..
2199
2265
 
2200
- if( this.selection )
2266
+ if( cursor.selection )
2201
2267
  {
2202
2268
  this.actions['Backspace'].callback( lidx, cursor, e );
2203
2269
  lidx = cursor.line;
@@ -2258,22 +2324,32 @@ class CodeEditor {
2258
2324
 
2259
2325
  let text = await navigator.clipboard.readText();
2260
2326
 
2327
+ // Remove any possible tabs (\t) and add spaces
2328
+ text = text.replaceAll( /\t|\\t/g, ' '.repeat( this.tabSpaces ) );
2329
+
2261
2330
  this._addUndoStep( cursor, true );
2262
2331
 
2263
2332
  this.appendText( text, cursor );
2333
+
2334
+ const currentScroll = this.getScrollTop();
2335
+ const scroll = Math.max( cursor.line * this.lineHeight - this.codeScroller.offsetWidth, 0 );
2336
+
2337
+ if( currentScroll < scroll ) {
2338
+ this.codeScroller.scrollTo( 0, cursor.line * this.lineHeight );
2339
+ }
2264
2340
  }
2265
2341
 
2266
2342
  async _copyContent( cursor ) {
2267
2343
 
2268
2344
  let text_to_copy = "";
2269
2345
 
2270
- if( !this.selection ) {
2346
+ if( !cursor.selection ) {
2271
2347
  text_to_copy = "\n" + this.code.lines[ cursor.line ];
2272
2348
  }
2273
2349
  else {
2274
2350
 
2275
2351
  // Some selections don't depend on mouse up..
2276
- if( this.selection ) this.selection.invertIfNecessary();
2352
+ if( cursor.selection ) cursor.selection.invertIfNecessary();
2277
2353
 
2278
2354
  const separator = "_NEWLINE_";
2279
2355
  let code = this.code.lines.join( separator );
@@ -2281,11 +2357,11 @@ class CodeEditor {
2281
2357
  // Get linear start index
2282
2358
  let index = 0;
2283
2359
 
2284
- for( let i = 0; i <= this.selection.fromY; i++ )
2285
- index += ( i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length );
2360
+ for( let i = 0; i <= cursor.selection.fromY; i++ )
2361
+ index += ( i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length );
2286
2362
 
2287
- index += this.selection.fromY * separator.length;
2288
- const num_chars = this.selection.chars + ( this.selection.toY - this.selection.fromY ) * separator.length;
2363
+ index += cursor.selection.fromY * separator.length;
2364
+ const num_chars = cursor.selection.chars + ( cursor.selection.toY - cursor.selection.fromY ) * separator.length;
2289
2365
  const text = code.substr( index, num_chars );
2290
2366
  const lines = text.split( separator );
2291
2367
  text_to_copy = lines.join('\n');
@@ -2301,7 +2377,7 @@ class CodeEditor {
2301
2377
 
2302
2378
  this._addUndoStep( cursor, true );
2303
2379
 
2304
- if( !this.selection ) {
2380
+ if( !cursor.selection ) {
2305
2381
  text_to_cut = "\n" + this.code.lines[ cursor.line ];
2306
2382
  this.code.lines.splice( lidx, 1 );
2307
2383
  this.processLines();
@@ -2312,7 +2388,7 @@ class CodeEditor {
2312
2388
  else {
2313
2389
 
2314
2390
  // Some selections don't depend on mouse up..
2315
- if( this.selection ) this.selection.invertIfNecessary();
2391
+ if( cursor.selection ) cursor.selection.invertIfNecessary();
2316
2392
 
2317
2393
  const separator = "_NEWLINE_";
2318
2394
  let code = this.code.lines.join(separator);
@@ -2320,11 +2396,11 @@ class CodeEditor {
2320
2396
  // Get linear start index
2321
2397
  let index = 0;
2322
2398
 
2323
- for(let i = 0; i <= this.selection.fromY; i++)
2324
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
2399
+ for(let i = 0; i <= cursor.selection.fromY; i++)
2400
+ index += (i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length);
2325
2401
 
2326
- index += this.selection.fromY * separator.length;
2327
- const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
2402
+ index += cursor.selection.fromY * separator.length;
2403
+ const num_chars = cursor.selection.chars + (cursor.selection.toY - cursor.selection.fromY) * separator.length;
2328
2404
  const text = code.substr(index, num_chars);
2329
2405
  const lines = text.split(separator);
2330
2406
  text_to_cut = lines.join('\n');
@@ -2345,6 +2421,114 @@ class CodeEditor {
2345
2421
  this.hideAutoCompleteBox();
2346
2422
  }
2347
2423
 
2424
+ _commentLines() {
2425
+
2426
+ this.state.keyChain = null;
2427
+
2428
+ if( cursor.selection )
2429
+ {
2430
+ var cursor = this._getCurrentCursor();
2431
+ this._addUndoStep( cursor, true );
2432
+
2433
+ const selectedLines = this.code.lines.slice( cursor.selection.fromY, cursor.selection.toY );
2434
+ const minIdx = Math.min(...selectedLines.map( v => {
2435
+ var idx = firstNonspaceIndex( v );
2436
+ return idx < 0 ? 1e10 : idx;
2437
+ } ));
2438
+
2439
+ for( var i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
2440
+ {
2441
+ this._commentLine( cursor, i, minIdx );
2442
+ }
2443
+ }
2444
+ else
2445
+ {
2446
+ for( let cursor of this.cursors.children )
2447
+ {
2448
+ this._addUndoStep( cursor, true );
2449
+ this._commentLine( cursor, cursor.line );
2450
+ }
2451
+ }
2452
+
2453
+ this.processLines();
2454
+ this._hideActiveLine();
2455
+ }
2456
+
2457
+ _commentLine( cursor, line, minNonspaceIdx ) {
2458
+
2459
+ const lang = this.languages[ this.highlight ];
2460
+
2461
+ if( !( lang.singleLineComments ?? true ))
2462
+ return;
2463
+
2464
+ const token = ( lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken ) + ' ';
2465
+ const string = this.code.lines[ line ];
2466
+
2467
+ let idx = firstNonspaceIndex( string );
2468
+ if( idx > -1 )
2469
+ {
2470
+ // Update idx using min of the selected lines (if necessary..)
2471
+ idx = minNonspaceIdx ?? idx;
2472
+
2473
+ this.code.lines[ line ] = [
2474
+ string.substring( 0, idx ),
2475
+ token,
2476
+ string.substring( idx )
2477
+ ].join( '' );
2478
+
2479
+ this.cursorToString( cursor, token );
2480
+ }
2481
+ }
2482
+
2483
+ _uncommentLines() {
2484
+
2485
+ this.state.keyChain = null;
2486
+
2487
+ if( cursor.selection )
2488
+ {
2489
+ var cursor = this._getCurrentCursor();
2490
+ this._addUndoStep( cursor, true );
2491
+
2492
+ for( var i = cursor.selection.fromY; i <= cursor.selection.toY; ++i )
2493
+ {
2494
+ this._uncommentLine( cursor, i );
2495
+ }
2496
+ }
2497
+ else
2498
+ {
2499
+ for( let cursor of this.cursors.children )
2500
+ {
2501
+ this._addUndoStep( cursor, true );
2502
+ this._uncommentLine( cursor, cursor.line );
2503
+ }
2504
+ }
2505
+
2506
+ this.processLines();
2507
+ this._hideActiveLine();
2508
+ }
2509
+
2510
+ _uncommentLine( cursor, line ) {
2511
+
2512
+ const lang = this.languages[ this.highlight ];
2513
+
2514
+ if( !( lang.singleLineComments ?? true ))
2515
+ return;
2516
+
2517
+ const token = lang.singleLineCommentToken ?? this.defaultSingleLineCommentToken;
2518
+ const string = this.code.lines[ line ];
2519
+
2520
+ if( string.includes( token ) )
2521
+ {
2522
+ this.code.lines[ line ] = string.replace( token + ' ', '' );
2523
+
2524
+ // try deleting token + space, and then if not, delete only the token
2525
+ if( string.length == this.code.lines[ line ].length )
2526
+ this.code.lines[ line ] = string.replace( token, '' );
2527
+
2528
+ this.cursorToString( cursor, 'X'.repeat( Math.abs( string.length - this.code.lines[ line ].length ) ), true );
2529
+ }
2530
+ }
2531
+
2348
2532
  action( key, deleteSelection, fn ) {
2349
2533
 
2350
2534
  this.actions[ key ] = {
@@ -2379,6 +2563,7 @@ class CodeEditor {
2379
2563
  processLines( mode ) {
2380
2564
 
2381
2565
  var code_html = "";
2566
+ this._blockCommentCache.length = 0;
2382
2567
 
2383
2568
  // Reset all lines content
2384
2569
  this.code.innerHTML = "";
@@ -2413,8 +2598,7 @@ class CodeEditor {
2413
2598
  this.code.style.top = ( this.visibleLinesViewport.x * this.lineHeight ) + "px";
2414
2599
 
2415
2600
  // Update selections
2416
- if( this.selection )
2417
- this.processSelection( null, true );
2601
+ this.updateSelections( null, true );
2418
2602
 
2419
2603
  this._clearTmpVariables();
2420
2604
  this._setActiveLine();
@@ -2423,6 +2607,13 @@ class CodeEditor {
2423
2607
 
2424
2608
  processLine( linenum, force ) {
2425
2609
 
2610
+ // Check if we are in block comment sections..
2611
+ if( !force && this._inBlockCommentSection( linenum ) )
2612
+ {
2613
+ this.processLines();
2614
+ return;
2615
+ }
2616
+
2426
2617
  const lang = this.languages[ this.highlight ];
2427
2618
  const local_line_num = this.toLocalLine( linenum );
2428
2619
  const gutter_line = "<span class='line-gutter'>" + (linenum + 1) + "</span>";
@@ -2431,6 +2622,7 @@ class CodeEditor {
2431
2622
  if( !force ) // Single line update
2432
2623
  {
2433
2624
  this.code.childNodes[ local_line_num ].innerHTML = gutter_line + html;
2625
+ this._setActiveLine( linenum );
2434
2626
  this._clearTmpVariables();
2435
2627
  }
2436
2628
  else // Update all lines at once
@@ -2457,6 +2649,9 @@ class CodeEditor {
2457
2649
  return UPDATE_LINE( linestring );
2458
2650
  }
2459
2651
 
2652
+ this._currentLineNumber = linenum;
2653
+ this._currentLineString = linestring;
2654
+
2460
2655
  const tokensToEvaluate = this._getTokensFromLine( linestring );
2461
2656
 
2462
2657
  if( !tokensToEvaluate.length )
@@ -2487,7 +2682,7 @@ class CodeEditor {
2487
2682
  {
2488
2683
  const blockCommentsToken = ( lang.blockCommentsTokens ?? this.defaultBlockCommentTokens )[ 0 ];
2489
2684
  if( token.substr( 0, blockCommentsToken.length ) == blockCommentsToken )
2490
- this._buildingBlockComment = true;
2685
+ this._buildingBlockComment = linenum;
2491
2686
  }
2492
2687
 
2493
2688
  line_inner_html += this._evaluateToken( {
@@ -2525,19 +2720,12 @@ class CodeEditor {
2525
2720
  var matches = (linestring.substring( 0, idx ).match( re ) || []);
2526
2721
  return (matches.length % 2) !== 0;
2527
2722
  } );
2528
- err |= stringKeys.some( function(v) {
2529
- var re = new RegExp( v, "g" );
2530
- var matches = (linestring.substring( idx ).match( re ) || []);
2531
- return (matches.length % 2) !== 0;
2532
- } );
2533
2723
  return err ? undefined : idx;
2534
2724
  }
2535
2725
  }
2536
2726
 
2537
2727
  _getTokensFromLine( linestring, skipNonWords ) {
2538
2728
 
2539
- this._currentLineString = linestring;
2540
-
2541
2729
  // Check if line comment
2542
2730
  const ogLine = linestring;
2543
2731
  const hasCommentIdx = this._lineHasComment( linestring );
@@ -2594,12 +2782,12 @@ class CodeEditor {
2594
2782
 
2595
2783
  if( this.highlight == 'C++' || this.highlight == 'CSS' )
2596
2784
  {
2597
- var idx = tokens.slice( offset ).findIndex( ( value, index ) => this.isNumber( value ) );
2785
+ var idx = tokens.slice( offset ).findIndex( ( value, index ) => this._isNumber( value ) );
2598
2786
  if( idx > -1 )
2599
2787
  {
2600
2788
  idx += offset; // Add offset to compute within the whole array of tokens
2601
2789
  let data = tokens[ idx ] + tokens[ ++idx ];
2602
- while( this.isNumber( data ) )
2790
+ while( this._isNumber( data ) )
2603
2791
  {
2604
2792
  tokens[ idx - 1 ] += tokens[ idx ];
2605
2793
  tokens.splice( idx, 1 );
@@ -2647,7 +2835,7 @@ class CodeEditor {
2647
2835
  // Manage strings
2648
2836
  this._stringEnded = false;
2649
2837
 
2650
- if( usePreviousTokenToCheckString || ( !this._buildingBlockComment && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
2838
+ if( usePreviousTokenToCheckString || ( this._buildingBlockComment === undefined && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) ) )
2651
2839
  {
2652
2840
  const checkIfStringEnded = t => {
2653
2841
  const idx = Object.values( customStringKeys ).indexOf( t );
@@ -2696,22 +2884,22 @@ class CodeEditor {
2696
2884
  else if( this._buildingString != undefined )
2697
2885
  discardToken = this._appendStringToken( token );
2698
2886
 
2699
- else if( this._mustHightlightWord( token, this.keywords ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2887
+ else if( this._mustHightlightWord( token, CodeEditor.keywords ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2700
2888
  token_classname = "cm-kwd";
2701
2889
 
2702
- else if( this._mustHightlightWord( token, this.builtin ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2890
+ else if( this._mustHightlightWord( token, CodeEditor.builtin ) && ( lang.tags ?? false ? ( this._enclosedByTokens( token, tokenIndex, '<', '>' ) ) : true ) )
2703
2891
  token_classname = "cm-bln";
2704
2892
 
2705
- else if( this._mustHightlightWord( token, this.statementsAndDeclarations ) )
2893
+ else if( this._mustHightlightWord( token, CodeEditor.statementsAndDeclarations ) )
2706
2894
  token_classname = "cm-std";
2707
2895
 
2708
- else if( this._mustHightlightWord( token, this.symbols ) )
2896
+ else if( this._mustHightlightWord( token, CodeEditor.symbols ) )
2709
2897
  token_classname = "cm-sym";
2710
2898
 
2711
2899
  else if( token.substr( 0, singleLineCommentToken.length ) == singleLineCommentToken )
2712
2900
  token_classname = "cm-com";
2713
2901
 
2714
- else if( this.isNumber( token ) || this.isNumber( token.replace(/[px]|[em]|%/g,'') ) )
2902
+ else if( this._isNumber( token ) || this._isNumber( token.replace(/[px]|[em]|%/g,'') ) )
2715
2903
  token_classname = "cm-dec";
2716
2904
 
2717
2905
  else if( this._isCSSClass( token, prev, next ) )
@@ -2723,7 +2911,7 @@ class CodeEditor {
2723
2911
  else if ( highlight == 'batch' && ( token == '@' || prev == ':' || prev == '@' ) )
2724
2912
  token_classname = "cm-kwd";
2725
2913
 
2726
- else if ( highlight == 'cpp' && token.includes( '#' ) ) // C++ preprocessor
2914
+ else if ( [ 'cpp', 'wgsl', 'glsl' ].indexOf( highlight ) > -1 && token.includes( '#' ) ) // C++ preprocessor
2727
2915
  token_classname = "cm-ppc";
2728
2916
 
2729
2917
  else if ( highlight == 'cpp' && prev == '<' && (next == '>' || next == '*') ) // Defining template type in C++
@@ -2748,9 +2936,10 @@ class CodeEditor {
2748
2936
  token_classname = "cm-mtd";
2749
2937
 
2750
2938
 
2751
- if( usesBlockComments && this._buildingBlockComment
2939
+ if( usesBlockComments && this._buildingBlockComment != undefined
2752
2940
  && token.substr( 0, blockCommentsTokens[ 1 ].length ) == blockCommentsTokens[ 1 ] )
2753
2941
  {
2942
+ this._blockCommentCache.push( new LX.vec2( this._buildingBlockComment, this._currentLineNumber ) );
2754
2943
  delete this._buildingBlockComment;
2755
2944
  }
2756
2945
 
@@ -2799,16 +2988,27 @@ class CodeEditor {
2799
2988
  _enclosedByTokens( token, tokenIndex, tagStart, tagEnd ) {
2800
2989
 
2801
2990
  const tokenStartIndex = this._currentTokenPositions[ tokenIndex ];
2802
- const tagStartIndex = indexOfFrom(this._currentLineString, tagStart, tokenStartIndex, true );
2991
+ const tagStartIndex = indexOfFrom( this._currentLineString, tagStart, tokenStartIndex, true );
2803
2992
  if( tagStartIndex < 0 ) // Not found..
2804
2993
  return;
2805
- const tagEndIndex = indexOfFrom(this._currentLineString, tagEnd, tokenStartIndex );
2994
+ const tagEndIndex = indexOfFrom( this._currentLineString, tagEnd, tokenStartIndex );
2806
2995
  if( tagEndIndex < 0 ) // Not found..
2807
2996
  return;
2808
2997
 
2809
2998
  return ( tagStartIndex < tokenStartIndex ) && ( tagEndIndex >= ( tokenStartIndex + token.length ) );
2810
2999
  }
2811
3000
 
3001
+ _inBlockCommentSection( line ) {
3002
+
3003
+ for( var section of this._blockCommentCache )
3004
+ {
3005
+ if( line >= section.x && line <= section.y )
3006
+ return true;
3007
+ }
3008
+
3009
+ return false;
3010
+ }
3011
+
2812
3012
  _isCSSClass( token, prev, next ) {
2813
3013
 
2814
3014
  if( this.highlight != 'CSS' )
@@ -2819,20 +3019,26 @@ class CodeEditor {
2819
3019
  || ( token[ 0 ] == '#' && prev != ':' ) );
2820
3020
  }
2821
3021
 
2822
- isNumber( token ) {
3022
+ _isNumber( token ) {
2823
3023
 
2824
3024
  if(this.highlight == 'C++')
2825
3025
  {
2826
3026
  if( token.lastChar == 'f' )
2827
- return this.isNumber( token.substring(0, token.length - 1) )
3027
+ return this._isNumber( token.substring(0, token.length - 1) )
2828
3028
  else if( token.lastChar == 'u' )
2829
- return !(token.includes('.')) && this.isNumber( token.substring(0, token.length - 1) );
3029
+ return !(token.includes('.')) && this._isNumber( token.substring(0, token.length - 1) );
3030
+ }
3031
+
3032
+ else if(this.highlight == 'WGSL')
3033
+ {
3034
+ if( token.lastChar == 'u' )
3035
+ return !(token.includes('.')) && this._isNumber( token.substring(0, token.length - 1) );
2830
3036
  }
2831
3037
 
2832
3038
  else if(this.highlight == 'CSS')
2833
3039
  {
2834
3040
  if( token.lastChar == '%' )
2835
- return this.isNumber( token.substring(0, token.length - 1) )
3041
+ return this._isNumber( token.substring(0, token.length - 1) )
2836
3042
  }
2837
3043
 
2838
3044
  return token.length && token != ' ' && !Number.isNaN(+token);
@@ -2841,7 +3047,7 @@ class CodeEditor {
2841
3047
  _isType( token, prev, next ) {
2842
3048
 
2843
3049
  // Common case
2844
- if( this._mustHightlightWord( token, this.types ) )
3050
+ if( this._mustHightlightWord( token, CodeEditor.types ) )
2845
3051
  return true;
2846
3052
 
2847
3053
  if( this.highlight == 'JavaScript' )
@@ -2854,30 +3060,31 @@ class CodeEditor {
2854
3060
  }
2855
3061
  else if ( this.highlight == 'WGSL' )
2856
3062
  {
2857
- const is_kwd = !this._mustHightlightWord( token, this.keywords );
2858
- return (prev == 'struct' && next == '{') ||
2859
- ( is_kwd &&
3063
+ const not_kwd = !this._mustHightlightWord( token, CodeEditor.keywords );
3064
+ return (prev == 'struct' && next == '{') ||
3065
+ (not_kwd && prev == ':' && next == ';') ||
3066
+ ( not_kwd &&
2860
3067
  ( prev == ':' && next == ')' || prev == ':' && next == ',' || prev == '>' && next == '{'
2861
- || prev == '<' && next == ',' || prev == '<' && next == '>' || prev == '>' && !next ));
3068
+ || prev == '<' && next == ',' || prev == '<' && next == '>' || prev == '>' && token != ';' && !next ));
2862
3069
  }
2863
3070
  }
2864
3071
 
2865
3072
  _encloseSelectedWordWithKey( key, lidx, cursor ) {
2866
3073
 
2867
- if( !this.selection || (this.selection.fromY != this.selection.toY) )
3074
+ if( !cursor.selection || (cursor.selection.fromY != cursor.selection.toY) )
2868
3075
  return false;
2869
3076
 
2870
- this.selection.invertIfNecessary();
3077
+ cursor.selection.invertIfNecessary();
2871
3078
 
2872
3079
  // Insert first..
2873
3080
  this.code.lines[ lidx ] = [
2874
- this.code.lines[ lidx ].slice(0, this.selection.fromX),
3081
+ this.code.lines[ lidx ].slice(0, cursor.selection.fromX),
2875
3082
  key,
2876
- this.code.lines[ lidx ].slice(this.selection.fromX)
3083
+ this.code.lines[ lidx ].slice(cursor.selection.fromX)
2877
3084
  ].join('');
2878
3085
 
2879
3086
  // Go to the end of the word
2880
- this.cursorToPosition(cursor, this.selection.toX + 1);
3087
+ this.cursorToPosition(cursor, cursor.selection.toX + 1);
2881
3088
 
2882
3089
  // Change next key?
2883
3090
  switch(key)
@@ -2898,10 +3105,10 @@ class CodeEditor {
2898
3105
 
2899
3106
  // Recompute and reposition current selection
2900
3107
 
2901
- this.selection.fromX++;
2902
- this.selection.toX++;
3108
+ cursor.selection.fromX++;
3109
+ cursor.selection.toX++;
2903
3110
 
2904
- this.processSelection();
3111
+ this._processSelection( cursor );
2905
3112
  this.processLine( lidx );
2906
3113
 
2907
3114
  // Stop propagation
@@ -2949,14 +3156,16 @@ class CodeEditor {
2949
3156
 
2950
3157
  startSelection( cursor ) {
2951
3158
 
2952
- // Clear other selections...
2953
- this.selections.innerHTML = "";
2954
-
2955
3159
  // Show elements
2956
- this.selections.classList.add( 'show' );
3160
+ let selectionContainer = document.createElement( 'div' );
3161
+ selectionContainer.className = 'selections';
3162
+ selectionContainer.classList.add( 'show' );
3163
+
3164
+ this.codeSizer.insertChildAtIndex( selectionContainer, 2 );
3165
+ this.selections[ cursor.name ] = selectionContainer;
2957
3166
 
2958
3167
  // Create new selection instance
2959
- this.selection = new CodeSelection( this, cursor.position, cursor.line );
3168
+ cursor.selection = new CodeSelection( this, cursor );
2960
3169
  }
2961
3170
 
2962
3171
  deleteSelection( cursor ) {
@@ -2966,37 +3175,51 @@ class CodeEditor {
2966
3175
  return;
2967
3176
 
2968
3177
  // Some selections don't depend on mouse up..
2969
- if( this.selection ) this.selection.invertIfNecessary();
3178
+ if( cursor.selection ) cursor.selection.invertIfNecessary();
2970
3179
 
2971
3180
  const separator = "_NEWLINE_";
2972
3181
  let code = this.code.lines.join( separator );
2973
3182
 
2974
3183
  // Get linear start index
2975
3184
  let index = 0;
2976
- for( let i = 0; i <= this.selection.fromY; i++ )
2977
- index += (i == this.selection.fromY ? this.selection.fromX : this.code.lines[ i ].length);
3185
+ for( let i = 0; i <= cursor.selection.fromY; i++ )
3186
+ index += (i == cursor.selection.fromY ? cursor.selection.fromX : this.code.lines[ i ].length);
2978
3187
 
2979
- index += this.selection.fromY * separator.length;
3188
+ index += cursor.selection.fromY * separator.length;
2980
3189
 
2981
- const num_chars = this.selection.chars + (this.selection.toY - this.selection.fromY) * separator.length;
3190
+ const num_chars = cursor.selection.chars + (cursor.selection.toY - cursor.selection.fromY) * separator.length;
2982
3191
  const pre = code.slice( 0, index );
2983
3192
  const post = code.slice( index + num_chars );
2984
3193
 
2985
3194
  this.code.lines = ( pre + post ).split( separator );
2986
3195
 
2987
- this.cursorToLine( cursor, this.selection.fromY, true );
2988
- this.cursorToPosition( cursor, this.selection.fromX );
2989
- this.endSelection();
3196
+ this.cursorToLine( cursor, cursor.selection.fromY, true );
3197
+ this.cursorToPosition( cursor, cursor.selection.fromX );
3198
+ this.endSelection( cursor );
2990
3199
  this.processLines();
2991
3200
  }
2992
3201
 
2993
- endSelection() {
3202
+ endSelection( cursor ) {
2994
3203
 
2995
- this.selections.classList.remove( 'show' );
2996
- this.selections.innerHTML = "";
2997
- delete this.selection;
2998
3204
  delete this._tripleClickSelection;
2999
3205
  delete this._lastSelectionKeyDir;
3206
+ delete this._currentOcurrences;
3207
+
3208
+ if( cursor )
3209
+ {
3210
+ deleteElement( this.selections[ cursor.name ] );
3211
+ delete this.selections[ cursor.name ];
3212
+ delete cursor.selection;
3213
+ }
3214
+ else
3215
+ {
3216
+ for( let cursor of this.cursors.children )
3217
+ {
3218
+ deleteElement( this.selections[ cursor.name ] );
3219
+ delete this.selections[ cursor.name ];
3220
+ delete cursor.selection;
3221
+ }
3222
+ }
3000
3223
  }
3001
3224
 
3002
3225
  selectAll() {
@@ -3010,13 +3233,13 @@ class CodeEditor {
3010
3233
  this.startSelection( cursor );
3011
3234
 
3012
3235
  const nlines = this.code.lines.length - 1;
3013
- this.selection.toX = this.code.lines[ nlines ].length;
3014
- this.selection.toY = nlines;
3236
+ cursor.selection.toX = this.code.lines[ nlines ].length;
3237
+ cursor.selection.toY = nlines;
3015
3238
 
3016
- this.cursorToPosition( cursor, this.selection.toX );
3017
- this.cursorToLine( cursor, this.selection.toY );
3239
+ this.cursorToPosition( cursor, cursor.selection.toX );
3240
+ this.cursorToLine( cursor, cursor.selection.toY );
3018
3241
 
3019
- this.processSelection( null, true );
3242
+ this._processSelection( cursor, null, true );
3020
3243
 
3021
3244
  this.hideAutoCompleteBox();
3022
3245
  }
@@ -3122,6 +3345,8 @@ class CodeEditor {
3122
3345
 
3123
3346
  state.position = cursor.position;
3124
3347
  state.line = cursor.line;
3348
+ state.selection = cursor.selection ? cursor.selection.save() : undefined;
3349
+
3125
3350
  return state;
3126
3351
  }
3127
3352
 
@@ -3154,7 +3379,7 @@ class CodeEditor {
3154
3379
  const cursorsInLine = Array.from( this.cursors.children ).filter( v => v.line == line );
3155
3380
 
3156
3381
  while( cursorsInLine.length > 1 )
3157
- cursorsInLine.pop().remove();
3382
+ this.removeCursor( cursorsInLine.pop() );
3158
3383
  }
3159
3384
 
3160
3385
  restoreCursor( cursor, state ) {
@@ -3166,6 +3391,22 @@ class CodeEditor {
3166
3391
  cursor.style.left = "calc(" + cursor._left + "px + " + this.xPadding + ")";
3167
3392
  cursor._top = cursor.line * this.lineHeight;
3168
3393
  cursor.style.top = "calc(" + cursor._top + "px)";
3394
+
3395
+ if( state.selection )
3396
+ {
3397
+ this.startSelection( cursor );
3398
+
3399
+ cursor.selection.load( state.selection );
3400
+
3401
+ this._processSelection( cursor, null, true );
3402
+ }
3403
+ }
3404
+
3405
+ removeCursor( cursor ) {
3406
+
3407
+ deleteElement( this.selections[ cursor.name ] );
3408
+ delete this.selections[ cursor.name ];
3409
+ deleteElement( cursor );
3169
3410
  }
3170
3411
 
3171
3412
  resetCursorPos( flag, cursor ) {
@@ -3486,11 +3727,11 @@ class CodeEditor {
3486
3727
 
3487
3728
  // Add language special keys...
3488
3729
  suggestions = suggestions.concat(
3489
- Object.keys( this.builtin[ this.highlight ] ?? {} ),
3490
- Object.keys( this.keywords[ this.highlight ] ?? {} ),
3491
- Object.keys( this.statementsAndDeclarations[ this.highlight ] ?? {} ),
3492
- Object.keys( this.types[ this.highlight ] ?? {} ),
3493
- Object.keys( this.utils[ this.highlight ] ?? {} )
3730
+ Object.keys( CodeEditor.builtin[ this.highlight ] ?? {} ),
3731
+ Object.keys( CodeEditor.keywords[ this.highlight ] ?? {} ),
3732
+ Object.keys( CodeEditor.statementsAndDeclarations[ this.highlight ] ?? {} ),
3733
+ Object.keys( CodeEditor.types[ this.highlight ] ?? {} ),
3734
+ Object.keys( CodeEditor.utils[ this.highlight ] ?? {} )
3494
3735
  );
3495
3736
 
3496
3737
  // Add words in current tab plus remove current word
@@ -3512,9 +3753,9 @@ class CodeEditor {
3512
3753
 
3513
3754
  var icon = document.createElement( 'a' );
3514
3755
 
3515
- if( this._mustHightlightWord( s, this.utils ) )
3756
+ if( this._mustHightlightWord( s, CodeEditor.utils ) )
3516
3757
  icon.className = "fa fa-cube";
3517
- else if( this._mustHightlightWord( s, this.types ) )
3758
+ else if( this._mustHightlightWord( s, CodeEditor.types ) )
3518
3759
  icon.className = "fa fa-code";
3519
3760
  else
3520
3761
  icon.className = "fa fa-font";
@@ -3630,6 +3871,8 @@ class CodeEditor {
3630
3871
 
3631
3872
  showSearchBox( clear ) {
3632
3873
 
3874
+ this.hideSearchLineBox();
3875
+
3633
3876
  this.searchbox.classList.add( 'opened' );
3634
3877
  this.searchboxActive = true;
3635
3878
 
@@ -3639,12 +3882,26 @@ class CodeEditor {
3639
3882
  {
3640
3883
  input.value = "";
3641
3884
  }
3885
+ else
3886
+ {
3887
+ const cursor = this._getCurrentCursor();
3888
+
3889
+ if( cursor.selection )
3890
+ {
3891
+ input.value = cursor.selection.getText() ?? input.value;
3892
+ }
3893
+ }
3894
+
3895
+ input.selectionStart = 0;
3896
+ input.selectionEnd = input.value.length;
3642
3897
 
3643
3898
  input.focus();
3644
3899
  }
3645
3900
 
3646
3901
  hideSearchBox() {
3647
3902
 
3903
+ const active = this.searchboxActive;
3904
+
3648
3905
  if( this.searchboxActive )
3649
3906
  {
3650
3907
  this.searchbox.classList.remove( 'opened' );
@@ -3653,31 +3910,49 @@ class CodeEditor {
3653
3910
 
3654
3911
  else if( this._lastResult )
3655
3912
  {
3656
- this._lastResult.dom.remove();
3913
+ deleteElement( this._lastResult.dom );
3657
3914
  delete this._lastResult;
3658
3915
  }
3916
+
3917
+ this.searchResultSelections.classList.remove( 'show' );
3918
+
3919
+ return active != this.searchboxActive;
3659
3920
  }
3660
3921
 
3661
- search( text, reverse ) {
3922
+ search( text, reverse, callback, skipAlert ) {
3662
3923
 
3663
3924
  text = text ?? this._lastTextFound;
3664
3925
 
3665
3926
  if( !text )
3666
3927
  return;
3667
3928
 
3668
- let cursorData = new LX.vec2( this.position, this.line );
3929
+ let cursor = this._getCurrentCursor();
3930
+ let cursorData = new LX.vec2( cursor.position, cursor.line );
3669
3931
  let line = null;
3670
3932
  let char = -1;
3671
3933
 
3672
3934
  if( this._lastResult )
3673
3935
  {
3674
- this._lastResult.dom.remove();
3936
+ deleteElement( this._lastResult.dom );
3675
3937
  cursorData = this._lastResult.pos;
3676
3938
  delete this._lastResult;
3677
3939
  }
3678
3940
 
3679
3941
  const getIndex = l => {
3680
- return this.code.lines[ l ].substr( l == cursorData.y ? cursorData.x : 0 ).indexOf( text );
3942
+
3943
+ var string = this.code.lines[ l ];
3944
+
3945
+ if( reverse )
3946
+ {
3947
+ string = string.substr( 0, l == cursorData.y ? cursorData.x : string.length );
3948
+ var reversed = strReverse( string );
3949
+ var reversedIdx = reversed.indexOf( strReverse( text ) );
3950
+ return reversedIdx == -1 ? -1 : string.length - reversedIdx - text.length;
3951
+ }
3952
+ else
3953
+ {
3954
+ return string.substr( l == cursorData.y ? cursorData.x : 0 ).indexOf( text );
3955
+ }
3681
3956
  };
3682
3957
 
3683
3958
  if( reverse )
@@ -3707,7 +3982,15 @@ class CodeEditor {
3707
3982
 
3708
3983
  if( line == null)
3709
3984
  {
3710
- alert("No results!")
3985
+ if( !skipAlert )
3986
+ alert( "No results!" );
3987
+
3988
+ const lastLine = this.code.lines.length - 1;
3989
+
3990
+ this._lastResult = {
3991
+ 'dom': this.searchResultSelections.lastChild,
3992
+ 'pos': reverse ? new LX.vec2( this.code.lines[ lastLine ].length, lastLine ) : new LX.vec2( 0, 0 )
3993
+ };
3711
3994
  return;
3712
3995
  }
3713
3996
 
@@ -3717,32 +4000,44 @@ class CodeEditor {
3717
4000
  have to add the length of the substring (0, first_ocurrence)
3718
4001
  */
3719
4002
 
3720
- char += ( line == cursorData.y ? cursorData.x : 0 );
4003
+
4004
+ if( !reverse )
4005
+ char += ( line == cursorData.y ? cursorData.x : 0 );
4006
+
3721
4007
 
3722
4008
  // Text found..
3723
4009
 
3724
4010
  this._lastTextFound = text;
3725
4011
 
3726
4012
  this.codeScroller.scrollTo(
3727
- Math.max( char * this.charWidth - this.codeScroller.clientWidth ),
3728
- Math.max( line - 10 ) * this.lineHeight
4013
+ Math.max( char * this.charWidth - this.codeScroller.clientWidth, 0 ),
4014
+ Math.max( line - 10, 0 ) * this.lineHeight
3729
4015
  );
3730
4016
 
3731
- // Show elements
3732
- this.selections.classList.add( 'show' );
4017
+ if( callback )
4018
+ {
4019
+ callback( char, line );
4020
+ }
4021
+ else
4022
+ {
4023
+ // Show elements
4024
+ this.searchResultSelections.classList.add( 'show' );
4025
+
4026
+ // Create new selection instance
4027
+ cursor.selection = new CodeSelection( this, cursor, "lexcodesearchresult" );
4028
+ cursor.selection.selectInline( cursor, char, line, this.measureString( text ), true );
4029
+ }
3733
4030
 
3734
- // Create new selection instance
3735
- this.selection = new CodeSelection( this, 0, 0, "lexcodesearchresult" );
3736
- this.selection.selectInline( char, line, this.measureString( text ) );
3737
4031
  this._lastResult = {
3738
- 'dom': this.selections.lastChild,
3739
- 'pos': new LX.vec2( char + text.length, line )
4032
+ 'dom': this.searchResultSelections.lastChild,
4033
+ 'pos': new LX.vec2( char + text.length * ( reverse ? -1 : 1 ) , line )
3740
4034
  };
3741
-
3742
4035
  }
3743
4036
 
3744
4037
  showSearchLineBox() {
3745
4038
 
4039
+ this.hideSearchBox();
4040
+
3746
4041
  this.searchlinebox.classList.add( 'opened' );
3747
4042
  this.searchlineboxActive = true;
3748
4043
 
@@ -3762,7 +4057,7 @@ class CodeEditor {
3762
4057
 
3763
4058
  goToLine( line ) {
3764
4059
 
3765
- if( !this.isNumber( line ) )
4060
+ if( !this._isNumber( line ) )
3766
4061
  return;
3767
4062
 
3768
4063
  this.codeScroller.scrollTo( 0, Math.max( line - 15 ) * this.lineHeight );
@@ -3772,6 +4067,40 @@ class CodeEditor {
3772
4067
  this.cursorToLine( cursor, line - 1, true );
3773
4068
  }
3774
4069
 
4070
+ selectNextOcurrence( cursor ) {
4071
+
4072
+ if( !cursor.selection )
4073
+ return;
4074
+
4075
+ const text = cursor.selection.getText();
4076
+ if( !text )
4077
+ return;
4078
+
4079
+ if( !this._currentOcurrences )
4080
+ {
4081
+ const currentKey = [ cursor.position - text.length, cursor.line ].join( '_' );
4082
+ this._currentOcurrences = { };
4083
+ this._currentOcurrences[ currentKey ] = true;
4084
+ }
4085
+
4086
+ this.search( text, false, (col, ln) => {
4087
+
4088
+ const key = [ col, ln ].join( '_' );
4089
+
4090
+ if( this._currentOcurrences[ key ] ) {
4091
+ return;
4092
+ }
4093
+
4094
+ var newCursor = this._addCursor( ln, col, true );
4095
+ this.startSelection( newCursor );
4096
+ newCursor.selection.selectInline( newCursor, col, ln, this.measureString( text ) );
4097
+ this.cursorToString( newCursor, text );
4098
+
4099
+ this._currentOcurrences[ key ] = true;
4100
+
4101
+ }, true );
4102
+ }
4103
+
3775
4104
  _updateDataInfoPanel( signal, value ) {
3776
4105
 
3777
4106
  if( !this.skipCodeInfo )
@@ -3809,6 +4138,11 @@ class CodeEditor {
3809
4138
  }
3810
4139
  }
3811
4140
 
4141
+ _hideActiveLine() {
4142
+
4143
+ this.code.querySelectorAll( '.active-line' ).forEach( e => e.classList.remove( 'active-line' ) );
4144
+ }
4145
+
3812
4146
  _increaseFontSize() {
3813
4147
 
3814
4148
  // Change font size
@@ -3864,13 +4198,96 @@ class CodeEditor {
3864
4198
  _clearTmpVariables() {
3865
4199
 
3866
4200
  delete this._currentLineString;
4201
+ delete this._currentLineNumber;
3867
4202
  delete this._buildingString;
3868
4203
  delete this._pendingString;
3869
4204
  delete this._buildingBlockComment;
3870
4205
  delete this._markdownHeader;
4206
+ delete this._lastResult;
3871
4207
  }
3872
4208
  }
3873
4209
 
4210
+ CodeEditor.keywords = {
4211
+
4212
+ 'JavaScript': ['var', 'let', 'const', 'this', 'in', 'of', 'true', 'false', 'new', 'function', 'NaN', 'static', 'class', 'constructor', 'null', 'typeof', 'debugger', 'abstract',
4213
+ 'arguments', 'extends', 'instanceof'],
4214
+ 'C++': ['int', 'float', 'double', 'bool', 'char', 'wchar_t', 'const', 'static_cast', 'dynamic_cast', 'new', 'delete', 'void', 'true', 'false', 'auto', 'struct', 'typedef', 'nullptr',
4215
+ 'NULL', 'unsigned', 'namespace'],
4216
+ 'JSON': ['true', 'false'],
4217
+ 'GLSL': ['true', 'false', 'function', 'int', 'float', 'vec2', 'vec3', 'vec4', 'mat2x2', 'mat3x3', 'mat4x4', 'struct'],
4218
+ 'CSS': ['body', 'html', 'canvas', 'div', 'input', 'span', '.'],
4219
+ 'WGSL': ['var', 'let', 'true', 'false', 'fn', 'bool', 'u32', 'i32', 'f16', 'f32', 'vec2f', 'vec3f', 'vec4f', 'mat2x2f', 'mat3x3f', 'mat4x4f', 'array', 'atomic', 'struct',
4220
+ 'sampler', 'sampler_comparison', 'texture_depth_2d', 'texture_depth_2d_array', 'texture_depth_cube', 'texture_depth_cube_array', 'texture_depth_multisampled_2d',
4221
+ 'texture_external', 'texture_1d', 'texture_2d', 'texture_2d_array', 'texture_3d', 'texture_cube', 'texture_cube_array', 'texture_storage_1d', 'texture_storage_2d',
4222
+ 'texture_storage_2d_array', 'texture_storage_3d', 'vec2u', 'vec3u', 'vec4u'],
4223
+ 'Rust': ['as', 'const', 'crate', 'enum', 'extern', 'false', 'fn', 'impl', 'in', 'let', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super', 'trait', 'true',
4224
+ 'type', 'unsafe', 'use', 'where', 'abstract', 'become', 'box', 'final', 'macro', 'override', 'priv', 'typeof', 'unsized', 'virtual'],
4225
+ 'Python': ['False', 'def', 'None', 'True', 'in', 'is', 'and', 'lambda', 'nonlocal', 'not', 'or'],
4226
+ 'Batch': ['set', 'SET', 'echo', 'ECHO', 'off', 'OFF', 'del', 'DEL', 'defined', 'DEFINED', 'setlocal', 'SETLOCAL', 'enabledelayedexpansion', 'ENABLEDELAYEDEXPANSION', 'driverquery',
4227
+ 'DRIVERQUERY', 'print', 'PRINT'],
4228
+ 'HTML': ['html', 'meta', 'title', 'link', 'script', 'body', 'DOCTYPE', 'head', 'br', 'i', 'a', 'li', 'img', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4229
+ 'Markdown': ['br', 'i', 'a', 'li', 'img', 'table', 'title', 'tr', 'td', 'h1', 'h2', 'h3', 'h4', 'h5'],
4230
+ };
4231
+
4232
+ CodeEditor.utils = { // These ones don't have hightlight, used as suggestions to autocomplete only...
4233
+
4234
+ 'JavaScript': ['querySelector', 'body', 'addEventListener', 'removeEventListener', 'remove', 'sort', 'keys', 'filter', 'isNaN', 'parseFloat', 'parseInt', 'EPSILON', 'isFinite',
4235
+ 'bind', 'prototype', 'length', 'assign', 'entries', 'values', 'concat', 'substring', 'substr', 'splice', 'slice', 'buffer', 'appendChild', 'createElement', 'prompt',
4236
+ 'alert'],
4237
+ 'WGSL': ['textureSample'],
4238
+ 'Python': ['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
4239
+ 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance',
4240
+ 'issubclass', 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr',
4241
+ 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
4242
+ };
4243
+
4244
+ CodeEditor.types = {
4245
+
4246
+ 'JavaScript': ['Object', 'String', 'Function', 'Boolean', 'Symbol', 'Error', 'Number', 'TextEncoder', 'TextDecoder'],
4247
+ 'Rust': ['u128'],
4248
+ 'Python': ['int', 'type', 'float', 'map', 'list', 'ArithmeticError', 'AssertionError', 'AttributeError', 'Exception', 'EOFError', 'FloatingPointError', 'GeneratorExit',
4249
+ 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'NotImplementedError', 'OSError',
4250
+ 'OverflowError', 'ReferenceError', 'RuntimeError', 'StopIteration', 'SyntaxError', 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
4251
+ 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'ValueError', 'ZeroDivisionError'],
4252
+ 'C++': ['uint8_t', 'uint16_t', 'uint32_t']
4253
+ };
4254
+
4255
+ CodeEditor.builtin = {
4256
+
4257
+ 'JavaScript': ['document', 'console', 'window', 'navigator', 'performance'],
4258
+ 'CSS': ['*', '!important'],
4259
+ 'C++': ['vector', 'list', 'map'],
4260
+ 'HTML': ['type', 'xmlns', 'PUBLIC', 'http-equiv', 'src', 'style', 'lang', 'href', 'rel', 'content', 'xml', 'alt'], // attributes
4261
+ 'Markdown': ['type', 'src', 'style', 'lang', 'href', 'rel', 'content', 'valign', 'alt'], // attributes
4262
+ };
4263
+
4264
+ CodeEditor.statementsAndDeclarations = {
4265
+
4266
+ 'JavaScript': ['for', 'if', 'else', 'case', 'switch', 'return', 'while', 'continue', 'break', 'do', 'import', 'from', 'throw', 'async', 'try', 'catch', 'await'],
4267
+ 'CSS': ['@', 'import'],
4268
+ 'C++': ['std', 'for', 'if', 'else', 'return', 'continue', 'break', 'case', 'switch', 'while', 'using', 'glm', 'spdlog'],
4269
+ 'GLSL': ['for', 'if', 'else', 'return', 'continue', 'break'],
4270
+ 'WGSL': ['const','for', 'if', 'else', 'return', 'continue', 'break', 'storage', 'read', 'read_write', 'uniform', 'function', 'workgroup'],
4271
+ 'Rust': ['break', 'else', 'continue', 'for', 'if', 'loop', 'match', 'return', 'while', 'do', 'yield'],
4272
+ 'Python': ['if', 'raise', 'del', 'import', 'return', 'elif', 'try', 'else', 'while', 'as', 'except', 'with', 'assert', 'finally', 'yield', 'break', 'for', 'class', 'continue',
4273
+ 'global', 'pass'],
4274
+ 'Batch': ['if', 'IF', 'for', 'FOR', 'in', 'IN', 'do', 'DO', 'call', 'CALL', 'goto', 'GOTO', 'exit', 'EXIT']
4275
+ };
4276
+
4277
+ CodeEditor.symbols = {
4278
+
4279
+ 'JavaScript': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '??'],
4280
+ 'C++': ['<', '>', '[', ']', '{', '}', '(', ')', ';', '=', '|', '||', '&', '&&', '?', '::', '*', '-', '+'],
4281
+ 'JSON': ['[', ']', '{', '}', '(', ')'],
4282
+ 'GLSL': ['[', ']', '{', '}', '(', ')'],
4283
+ 'WGSL': ['[', ']', '{', '}', '(', ')', '->'],
4284
+ 'CSS': ['{', '}', '(', ')', '*'],
4285
+ 'Rust': ['<', '>', '[', ']', '(', ')', '='],
4286
+ 'Python': ['<', '>', '[', ']', '(', ')', '='],
4287
+ 'Batch': ['[', ']', '(', ')', '%'],
4288
+ 'HTML': ['<', '>', '/']
4289
+ };
4290
+
3874
4291
  LX.CodeEditor = CodeEditor;
3875
4292
 
3876
4293
  export { CodeEditor };