lexgui 8.3.0 → 8.3.1

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.
@@ -16,7 +16,7 @@
16
16
  exports.LX = g$2.LX;
17
17
  if (!exports.LX) {
18
18
  exports.LX = {
19
- version: '8.3.0',
19
+ version: '8.3.1',
20
20
  ready: false,
21
21
  extensions: [], // Store extensions used
22
22
  extraCommandbarEntries: [], // User specific entries for command bar
@@ -9799,9 +9799,25 @@
9799
9799
  * @param {String} str
9800
9800
  */
9801
9801
  function toKebabCase(str) {
9802
- return str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
9802
+ return str
9803
+ .replace(/([A-Z])/g, '-$1')
9804
+ .replace(/[\s_]+/g, '-')
9805
+ .replace(/^-/, '')
9806
+ .toLowerCase();
9803
9807
  }
9804
9808
  exports.LX.toKebabCase = toKebabCase;
9809
+ /**
9810
+ * @method toSnakeCase
9811
+ * @param {String} str
9812
+ */
9813
+ function toSnakeCase(str) {
9814
+ return str
9815
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
9816
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
9817
+ .replace(/[\s\-]+/g, '_')
9818
+ .toLowerCase();
9819
+ }
9820
+ exports.LX.toSnakeCase = toSnakeCase;
9805
9821
  /**
9806
9822
  * @method getSupportedDOMName
9807
9823
  * @description Convert a text string to a valid DOM name
@@ -15595,6 +15611,21 @@
15595
15611
  this._inputArea.addEventListener('blur', () => this._setFocused(false));
15596
15612
  this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
15597
15613
  this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
15614
+ this.codeArea.root.addEventListener('mouseover', (e) => {
15615
+ const link = e.target.closest('.code-link');
15616
+ if (link && e.ctrlKey)
15617
+ link.classList.add('hovered');
15618
+ });
15619
+ this.codeArea.root.addEventListener('mouseout', (e) => {
15620
+ const link = e.target.closest('.code-link');
15621
+ if (link)
15622
+ link.classList.remove('hovered');
15623
+ });
15624
+ this.codeArea.root.addEventListener('mousemove', (e) => {
15625
+ const link = e.target.closest('.code-link');
15626
+ if (link)
15627
+ link.classList.toggle('hovered', e.ctrlKey);
15628
+ });
15598
15629
  // Bottom status panel
15599
15630
  this.statusPanel = this._createStatusPanel(options);
15600
15631
  if (this.statusPanel) {
@@ -15682,18 +15713,148 @@
15682
15713
  }
15683
15714
  }
15684
15715
  ;
15685
- setText(text) {
15716
+ setText(text, language, detectLang = false) {
15686
15717
  if (!this.currentTab)
15687
15718
  return;
15688
15719
  this.doc.setText(text);
15689
15720
  this.cursorSet.set(0, 0);
15690
15721
  this.undoManager.clear();
15691
15722
  this._lineStates = [];
15723
+ if (language) {
15724
+ this.setLanguage(language);
15725
+ }
15726
+ else if (detectLang) {
15727
+ const detected = this._detectLanguage(text);
15728
+ if (detected)
15729
+ this.setLanguage(detected);
15730
+ }
15692
15731
  this._renderAllLines();
15693
15732
  this._renderCursors();
15694
15733
  this._renderSelections();
15695
15734
  this.resize(true);
15696
15735
  }
15736
+ _detectLanguage(text) {
15737
+ const scores = {};
15738
+ const add = (lang, pts) => { scores[lang] = (scores[lang] ?? 0) + pts; };
15739
+ // Score using reservedWords from each registered language
15740
+ const textWords = new Set(text.match(/\b\w+\b/g) ?? []);
15741
+ const totalWords = Math.max(textWords.size, 1);
15742
+ for (const langName of Tokenizer.getRegisteredLanguages()) {
15743
+ const langDef = Tokenizer.getLanguage(langName);
15744
+ if (!langDef?.reservedWords?.length)
15745
+ continue;
15746
+ let hits = 0;
15747
+ for (const word of langDef.reservedWords) {
15748
+ if (textWords.has(word))
15749
+ hits++;
15750
+ }
15751
+ if (hits > 0) {
15752
+ const vocabRatio = hits / langDef.reservedWords.length;
15753
+ const textRatio = hits / totalWords;
15754
+ add(langName, Math.round((vocabRatio + textRatio) * 40));
15755
+ }
15756
+ }
15757
+ // Add scores based on "important" structural words only
15758
+ // HTML
15759
+ if (/<!DOCTYPE\s+html/i.test(text))
15760
+ add('HTML', 20);
15761
+ if (/<html[\s>]/i.test(text))
15762
+ add('HTML', 15);
15763
+ if (/<\/?(div|span|body|head|script|style|meta)\b/i.test(text))
15764
+ add('HTML', 8);
15765
+ // JSON — must come before JS checks (starts with { or [)
15766
+ if (/^\s*[\[{]/.test(text) && /"\s*:\s*/.test(text) && !/function|=>|const|var|let/.test(text))
15767
+ add('JSON', 15);
15768
+ // CSS
15769
+ if (/[\w-]+\s*:\s*[\w#\d"'(]+.*;/.test(text) && /[{}]/.test(text) && !/<\w+/.test(text))
15770
+ add('CSS', 10);
15771
+ if (/@(media|keyframes|import|charset|font-face)\b/.test(text))
15772
+ add('CSS', 15);
15773
+ // WGSL
15774
+ if (/@(vertex|fragment|compute|group|binding|builtin)\b/.test(text))
15775
+ add('WGSL', 20);
15776
+ if (/\bfn\s+\w+\s*\(/.test(text) && /\bvar\b/.test(text))
15777
+ add('WGSL', 10);
15778
+ if (/\b(vec2f|vec3f|vec4f|mat4x4f|f32|u32|i32)\b/.test(text))
15779
+ add('WGSL', 12);
15780
+ // GLSL
15781
+ if (/\b(gl_Position|gl_FragColor|gl_FragCoord)\b/.test(text))
15782
+ add('GLSL', 20);
15783
+ if (/\b(uniform|varying|attribute)\s+\w/.test(text))
15784
+ add('GLSL', 10);
15785
+ if (/\b(vec2|vec3|vec4|mat4|sampler2D)\b/.test(text) && !/vec2f|vec3f/.test(text))
15786
+ add('GLSL', 8);
15787
+ // HLSL
15788
+ if (/\b(SV_Position|SV_Target|SV_Depth)\b/.test(text))
15789
+ add('HLSL', 20);
15790
+ if (/\b(cbuffer|tbuffer|float4|float3|float2|Texture2D)\b/.test(text))
15791
+ add('HLSL', 12);
15792
+ // Python
15793
+ if (/^\s*def\s+\w+\s*\(/m.test(text))
15794
+ add('Python', 15);
15795
+ if (/^\s*import\s+\w/m.test(text) && !/\bfrom\s+['"]/.test(text))
15796
+ add('Python', 8);
15797
+ if (/^\s*class\s+\w+(\s*\(.*\))?:/m.test(text))
15798
+ add('Python', 10);
15799
+ if (/\bprint\s*\(/.test(text) && /:\s*$/.test(text))
15800
+ add('Python', 5);
15801
+ if (/elif\b|lambda\b|self\.\w/.test(text))
15802
+ add('Python', 8);
15803
+ // Rust
15804
+ if (/\bfn\s+\w+\s*\(/.test(text) && /\blet\s+mut\b/.test(text))
15805
+ add('Rust', 15);
15806
+ if (/\b(impl|trait|enum|struct)\s+\w+/.test(text) && /\bfn\b/.test(text))
15807
+ add('Rust', 12);
15808
+ if (/use\s+std::|use\s+\w+::\w+/.test(text))
15809
+ add('Rust', 10);
15810
+ if (/println!\s*\(/.test(text))
15811
+ add('Rust', 8);
15812
+ // C++
15813
+ if (/#include\s*<[\w./]+>/.test(text))
15814
+ add('C++', 15);
15815
+ if (/\bstd::\w+/.test(text))
15816
+ add('C++', 12);
15817
+ if (/\b(class|template|namespace|nullptr|new\s+\w)\b/.test(text))
15818
+ add('C++', 8);
15819
+ if (/(::|->)\w+/.test(text))
15820
+ add('C++', 6);
15821
+ // C
15822
+ if (/#include\s*<[\w.]+\.h>/.test(text))
15823
+ add('C', 12);
15824
+ if (/\bint\s+main\s*\(/.test(text))
15825
+ add('C', 15);
15826
+ if (/\b(printf|scanf|malloc|free|sizeof)\s*\(/.test(text))
15827
+ add('C', 10);
15828
+ // TypeScript — before JS (is a superset)
15829
+ if (/:\s*(string|number|boolean|void|any|never|unknown)\b/.test(text))
15830
+ add('TypeScript', 12);
15831
+ if (/\binterface\s+\w+/.test(text))
15832
+ add('TypeScript', 12);
15833
+ if (/\btype\s+\w+\s*=/.test(text))
15834
+ add('TypeScript', 10);
15835
+ if (/\bas\s+(string|number|any|unknown)\b/.test(text))
15836
+ add('TypeScript', 8);
15837
+ if (/\benum\s+\w+\s*\{/.test(text))
15838
+ add('TypeScript', 8);
15839
+ // JavaScript
15840
+ if (/\b(const|let|var)\s+\w+\s*=/.test(text))
15841
+ add('JavaScript', 8);
15842
+ if (/=>\s*[\w{(]/.test(text))
15843
+ add('JavaScript', 6);
15844
+ if (/\b(import|export)\s+(default|{|\*)/.test(text))
15845
+ add('JavaScript', 8);
15846
+ if (/\bconsole\.(log|warn|error)\b/.test(text))
15847
+ add('JavaScript', 6);
15848
+ // Markdown
15849
+ if (/^#{1,6}\s+\S/m.test(text))
15850
+ add('Markdown', 12);
15851
+ if (/\*\*\w.*?\*\*/.test(text) || /\[.+\]\(.+\)/.test(text))
15852
+ add('Markdown', 8);
15853
+ if (/^```\w*/m.test(text))
15854
+ add('Markdown', 10);
15855
+ const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
15856
+ return best && best[1] >= 8 ? best[0] : null;
15857
+ }
15697
15858
  appendText(text) {
15698
15859
  const cursor = this.cursorSet.getPrimary();
15699
15860
  const { line, col } = cursor.head;
@@ -16192,8 +16353,8 @@
16192
16353
  : Tokenizer.initialState();
16193
16354
  const lineText = this.doc.getLine(lineIndex);
16194
16355
  const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
16195
- // Build HTML
16196
16356
  const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
16357
+ const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
16197
16358
  let html = '';
16198
16359
  for (const token of result.tokens) {
16199
16360
  const cls = TOKEN_CLASS_MAP[token.type];
@@ -16201,11 +16362,15 @@
16201
16362
  .replace(/&/g, '&amp;')
16202
16363
  .replace(/</g, '&lt;')
16203
16364
  .replace(/>/g, '&gt;');
16365
+ // Wrap URLs in comment tokens with a clickable span
16366
+ const content = (token.type === 'comment')
16367
+ ? escaped.replace(URL_REGEX, `<span class="code-link" data-url="$1">$1</span>`)
16368
+ : escaped;
16204
16369
  if (cls) {
16205
- html += `<span class="${cls} ${langClass}">${escaped}</span>`;
16370
+ html += `<span class="${cls} ${langClass}">${content}</span>`;
16206
16371
  }
16207
16372
  else {
16208
- html += escaped;
16373
+ html += content;
16209
16374
  }
16210
16375
  }
16211
16376
  return { html: html || '&nbsp;', endState: result.state, tokens: result.tokens };
@@ -17035,11 +17200,25 @@
17035
17200
  const { line, col } = cursor.head;
17036
17201
  const indent = this.doc.getIndent(line);
17037
17202
  const spaces = ' '.repeat(indent);
17038
- const op = this.doc.insert(line, col, '\n' + spaces);
17039
- this.undoManager.record(op, this.cursorSet.getCursorPositions());
17040
- cursor.head = { line: line + 1, col: indent };
17041
- cursor.anchor = { ...cursor.head };
17042
- this.cursorSet.adjustOthers(idx, line, col, 0, 1);
17203
+ const charBefore = this.doc.getCharAt(line, col - 1);
17204
+ const charAfter = this.doc.getCharAt(line, col);
17205
+ const OPEN_CLOSE = { '{': '}', '[': ']', '(': ')' };
17206
+ if (charBefore && charAfter && OPEN_CLOSE[charBefore] === charAfter) {
17207
+ const innerSpaces = ' '.repeat(indent + this.tabSize);
17208
+ const insertion = '\n' + innerSpaces + '\n' + spaces;
17209
+ const op = this.doc.insert(line, col, insertion);
17210
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17211
+ cursor.head = { line: line + 1, col: indent + this.tabSize };
17212
+ cursor.anchor = { ...cursor.head };
17213
+ this.cursorSet.adjustOthers(idx, line, col, 0, 2);
17214
+ }
17215
+ else {
17216
+ const op = this.doc.insert(line, col, '\n' + spaces);
17217
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17218
+ cursor.head = { line: line + 1, col: indent };
17219
+ cursor.anchor = { ...cursor.head };
17220
+ this.cursorSet.adjustOthers(idx, line, col, 0, 1);
17221
+ }
17043
17222
  }
17044
17223
  this._rebuildLines();
17045
17224
  this._afterCursorMove();
@@ -17053,8 +17232,36 @@
17053
17232
  for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
17054
17233
  const cursor = this.cursorSet.cursors[idx];
17055
17234
  const { line, col } = cursor.head;
17056
- if (shift) {
17057
- // Dedent: remove up to tabSize spaces from start
17235
+ const anchorLine = cursor.anchor.line;
17236
+ // Multiline selection: indent/dedent all lines in the selection
17237
+ const startLine = Math.min(line, anchorLine);
17238
+ const endLine = Math.max(line, anchorLine);
17239
+ const isMultiline = startLine !== endLine;
17240
+ if (isMultiline) {
17241
+ for (let i = startLine; i <= endLine; i++) {
17242
+ if (shift) {
17243
+ const lineText = this.doc.getLine(i);
17244
+ let spacesToRemove = 0;
17245
+ while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
17246
+ spacesToRemove++;
17247
+ }
17248
+ if (spacesToRemove > 0) {
17249
+ const op = this.doc.delete(i, 0, spacesToRemove);
17250
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17251
+ }
17252
+ }
17253
+ else {
17254
+ const spaces = ' '.repeat(this.tabSize);
17255
+ const op = this.doc.insert(i, 0, spaces);
17256
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17257
+ }
17258
+ }
17259
+ const delta = shift ? -this.tabSize : this.tabSize;
17260
+ cursor.head = { line, col: Math.max(0, col + delta) };
17261
+ cursor.anchor = { line: anchorLine, col: Math.max(0, cursor.anchor.col + delta) };
17262
+ }
17263
+ else if (shift) {
17264
+ // Single line dedent: remove up to tabSize spaces from start
17058
17265
  const lineText = this.doc.getLine(line);
17059
17266
  let spacesToRemove = 0;
17060
17267
  while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
@@ -17069,6 +17276,7 @@
17069
17276
  }
17070
17277
  }
17071
17278
  else {
17279
+ // Single line indent: insert spaces at cursor
17072
17280
  const spacesToAdd = this.tabSize - (col % this.tabSize);
17073
17281
  const spaces = ' '.repeat(spacesToAdd);
17074
17282
  const op = this.doc.insert(line, col, spaces);
@@ -17102,8 +17310,10 @@
17102
17310
  this.cursorSet.adjustOthers(idx, start.line, start.col, -colDelta, -linesRemoved);
17103
17311
  anyDeleted = true;
17104
17312
  }
17105
- if (anyDeleted)
17313
+ if (anyDeleted) {
17106
17314
  this._rebuildLines();
17315
+ this._doHideAutocomplete();
17316
+ }
17107
17317
  }
17108
17318
  // Clipboard helpers:
17109
17319
  _doCopy() {
@@ -17217,6 +17427,15 @@
17217
17427
  return;
17218
17428
  if (this.autocomplete && this.autocomplete.contains(e.target))
17219
17429
  return;
17430
+ // Ctrl+click: open link if cursor is over a code-link span
17431
+ if (e.ctrlKey && e.button === 0) {
17432
+ const target = e.target;
17433
+ const link = target.closest('.code-link');
17434
+ if (link?.dataset.url) {
17435
+ window.open(link.dataset.url, '_blank');
17436
+ return;
17437
+ }
17438
+ }
17220
17439
  e.preventDefault(); // Prevent browser from stealing focus from _inputArea
17221
17440
  this._wasPaired = false;
17222
17441
  // Calculate line and column from click position
@@ -17334,9 +17553,9 @@
17334
17553
  }
17335
17554
  const suggestions = [];
17336
17555
  const added = new Set();
17337
- const addSuggestion = (label, kind, scope, detail) => {
17556
+ const addSuggestion = (label, kind, scope, detail, insertText) => {
17338
17557
  if (!added.has(label)) {
17339
- suggestions.push({ label, kind, scope, detail });
17558
+ suggestions.push({ label, kind, scope, detail, insertText });
17340
17559
  added.add(label);
17341
17560
  }
17342
17561
  };
@@ -17358,8 +17577,9 @@
17358
17577
  const label = typeof suggestion === 'string' ? suggestion : suggestion.label;
17359
17578
  const kind = typeof suggestion === 'object' ? suggestion.kind : undefined;
17360
17579
  const detail = typeof suggestion === 'object' ? suggestion.detail : undefined;
17580
+ const insertText = typeof suggestion === 'object' ? suggestion.insertText : suggestion;
17361
17581
  if (label.toLowerCase().startsWith(word.toLowerCase())) {
17362
- addSuggestion(label, kind, undefined, detail);
17582
+ addSuggestion(label, kind, undefined, detail, insertText);
17363
17583
  }
17364
17584
  }
17365
17585
  // Close autocomplete if no suggestions
@@ -17379,6 +17599,7 @@
17379
17599
  // Render suggestions
17380
17600
  suggestions.forEach((suggestion, index) => {
17381
17601
  const item = document.createElement('pre');
17602
+ item.insertText = suggestion.insertText ?? suggestion.label;
17382
17603
  if (index === this._selectedAutocompleteIndex)
17383
17604
  item.classList.add('selected');
17384
17605
  const currSuggestion = suggestion.label;
@@ -17497,8 +17718,8 @@
17497
17718
  * Insert the selected autocomplete word at cursor.
17498
17719
  */
17499
17720
  _doAutocompleteWord() {
17500
- const word = this._getSelectedAutoCompleteWord();
17501
- if (!word)
17721
+ const text = this._getSelectedAutoCompleteWord();
17722
+ if (!text)
17502
17723
  return;
17503
17724
  const cursor = this.cursorSet.getPrimary().head;
17504
17725
  const { start, end } = this._getWordAtCursor();
@@ -17508,8 +17729,14 @@
17508
17729
  const deleteOp = this.doc.delete(line, start, end - start);
17509
17730
  this.undoManager.record(deleteOp, cursorsBefore);
17510
17731
  }
17511
- const insertOp = this.doc.insert(line, start, word);
17512
- this.cursorSet.set(line, start + word.length);
17732
+ const insertOp = this.doc.insert(line, start, text);
17733
+ const insertedLines = text.split(/\r?\n/);
17734
+ if (insertedLines.length === 1) {
17735
+ this.cursorSet.set(line, start + text.length);
17736
+ }
17737
+ else {
17738
+ this.cursorSet.set(line + insertedLines.length - 1, insertedLines[insertedLines.length - 1].length);
17739
+ }
17513
17740
  const cursorsAfter = this.cursorSet.getCursorPositions();
17514
17741
  this.undoManager.record(insertOp, cursorsAfter);
17515
17742
  this._rebuildLines();
@@ -17520,15 +17747,7 @@
17520
17747
  if (!this.autocomplete || !this._isAutoCompleteActive)
17521
17748
  return null;
17522
17749
  const pre = this.autocomplete.childNodes[this._selectedAutocompleteIndex];
17523
- var word = '';
17524
- for (let childSpan of pre.childNodes) {
17525
- const span = childSpan;
17526
- if (span.constructor != HTMLSpanElement || span.classList.contains('kind')) {
17527
- continue;
17528
- }
17529
- word += span.textContent;
17530
- }
17531
- return word;
17750
+ return pre.insertText;
17532
17751
  }
17533
17752
  _afterCursorMove() {
17534
17753
  this._renderCursors();
@@ -17561,7 +17780,8 @@
17561
17780
  _resetGutter() {
17562
17781
  // Use cached value or compute if not available (e.g., on initial load)
17563
17782
  const tabsHeight = this._cachedTabsHeight || (this.tabs?.root.getBoundingClientRect().height ?? 0);
17564
- this.lineGutter.style.height = `calc(100% - ${tabsHeight}px)`;
17783
+ const statusPanelHeight = this._cachedStatusPanelHeight || (this.statusPanel?.root.getBoundingClientRect().height ?? 0);
17784
+ this.lineGutter.style.height = `calc(100% - ${tabsHeight + statusPanelHeight}px)`;
17565
17785
  }
17566
17786
  getMaxLineLength() {
17567
17787
  if (!this.currentTab)
@@ -27589,8 +27809,7 @@
27589
27809
  }
27590
27810
  else {
27591
27811
  [videoArea, controlsArea] = area.split({ type: 'vertical',
27592
- sizes: [controlsOptions.height ? `calc(100% - ${controlsOptions.height})` : '85%', null], minimizable: false,
27593
- resize: false });
27812
+ sizes: [controlsOptions.height ? `calc(100% - ${controlsOptions.height})` : '85%', null], minimizable: false, resize: false });
27594
27813
  }
27595
27814
  controlsArea.root.classList.add('lexconstrolsarea');
27596
27815
  this.cropArea = document.createElement('div');