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.
@@ -12,7 +12,7 @@ const g$2 = globalThis;
12
12
  let LX = g$2.LX;
13
13
  if (!LX) {
14
14
  LX = {
15
- version: '8.3.0',
15
+ version: '8.3.1',
16
16
  ready: false,
17
17
  extensions: [], // Store extensions used
18
18
  extraCommandbarEntries: [], // User specific entries for command bar
@@ -9795,9 +9795,25 @@ LX.toTitleCase = toTitleCase;
9795
9795
  * @param {String} str
9796
9796
  */
9797
9797
  function toKebabCase(str) {
9798
- return str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
9798
+ return str
9799
+ .replace(/([A-Z])/g, '-$1')
9800
+ .replace(/[\s_]+/g, '-')
9801
+ .replace(/^-/, '')
9802
+ .toLowerCase();
9799
9803
  }
9800
9804
  LX.toKebabCase = toKebabCase;
9805
+ /**
9806
+ * @method toSnakeCase
9807
+ * @param {String} str
9808
+ */
9809
+ function toSnakeCase(str) {
9810
+ return str
9811
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
9812
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
9813
+ .replace(/[\s\-]+/g, '_')
9814
+ .toLowerCase();
9815
+ }
9816
+ LX.toSnakeCase = toSnakeCase;
9801
9817
  /**
9802
9818
  * @method getSupportedDOMName
9803
9819
  * @description Convert a text string to a valid DOM name
@@ -15591,6 +15607,21 @@ class CodeEditor {
15591
15607
  this._inputArea.addEventListener('blur', () => this._setFocused(false));
15592
15608
  this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
15593
15609
  this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
15610
+ this.codeArea.root.addEventListener('mouseover', (e) => {
15611
+ const link = e.target.closest('.code-link');
15612
+ if (link && e.ctrlKey)
15613
+ link.classList.add('hovered');
15614
+ });
15615
+ this.codeArea.root.addEventListener('mouseout', (e) => {
15616
+ const link = e.target.closest('.code-link');
15617
+ if (link)
15618
+ link.classList.remove('hovered');
15619
+ });
15620
+ this.codeArea.root.addEventListener('mousemove', (e) => {
15621
+ const link = e.target.closest('.code-link');
15622
+ if (link)
15623
+ link.classList.toggle('hovered', e.ctrlKey);
15624
+ });
15594
15625
  // Bottom status panel
15595
15626
  this.statusPanel = this._createStatusPanel(options);
15596
15627
  if (this.statusPanel) {
@@ -15678,18 +15709,148 @@ class CodeEditor {
15678
15709
  }
15679
15710
  }
15680
15711
  ;
15681
- setText(text) {
15712
+ setText(text, language, detectLang = false) {
15682
15713
  if (!this.currentTab)
15683
15714
  return;
15684
15715
  this.doc.setText(text);
15685
15716
  this.cursorSet.set(0, 0);
15686
15717
  this.undoManager.clear();
15687
15718
  this._lineStates = [];
15719
+ if (language) {
15720
+ this.setLanguage(language);
15721
+ }
15722
+ else if (detectLang) {
15723
+ const detected = this._detectLanguage(text);
15724
+ if (detected)
15725
+ this.setLanguage(detected);
15726
+ }
15688
15727
  this._renderAllLines();
15689
15728
  this._renderCursors();
15690
15729
  this._renderSelections();
15691
15730
  this.resize(true);
15692
15731
  }
15732
+ _detectLanguage(text) {
15733
+ const scores = {};
15734
+ const add = (lang, pts) => { scores[lang] = (scores[lang] ?? 0) + pts; };
15735
+ // Score using reservedWords from each registered language
15736
+ const textWords = new Set(text.match(/\b\w+\b/g) ?? []);
15737
+ const totalWords = Math.max(textWords.size, 1);
15738
+ for (const langName of Tokenizer.getRegisteredLanguages()) {
15739
+ const langDef = Tokenizer.getLanguage(langName);
15740
+ if (!langDef?.reservedWords?.length)
15741
+ continue;
15742
+ let hits = 0;
15743
+ for (const word of langDef.reservedWords) {
15744
+ if (textWords.has(word))
15745
+ hits++;
15746
+ }
15747
+ if (hits > 0) {
15748
+ const vocabRatio = hits / langDef.reservedWords.length;
15749
+ const textRatio = hits / totalWords;
15750
+ add(langName, Math.round((vocabRatio + textRatio) * 40));
15751
+ }
15752
+ }
15753
+ // Add scores based on "important" structural words only
15754
+ // HTML
15755
+ if (/<!DOCTYPE\s+html/i.test(text))
15756
+ add('HTML', 20);
15757
+ if (/<html[\s>]/i.test(text))
15758
+ add('HTML', 15);
15759
+ if (/<\/?(div|span|body|head|script|style|meta)\b/i.test(text))
15760
+ add('HTML', 8);
15761
+ // JSON — must come before JS checks (starts with { or [)
15762
+ if (/^\s*[\[{]/.test(text) && /"\s*:\s*/.test(text) && !/function|=>|const|var|let/.test(text))
15763
+ add('JSON', 15);
15764
+ // CSS
15765
+ if (/[\w-]+\s*:\s*[\w#\d"'(]+.*;/.test(text) && /[{}]/.test(text) && !/<\w+/.test(text))
15766
+ add('CSS', 10);
15767
+ if (/@(media|keyframes|import|charset|font-face)\b/.test(text))
15768
+ add('CSS', 15);
15769
+ // WGSL
15770
+ if (/@(vertex|fragment|compute|group|binding|builtin)\b/.test(text))
15771
+ add('WGSL', 20);
15772
+ if (/\bfn\s+\w+\s*\(/.test(text) && /\bvar\b/.test(text))
15773
+ add('WGSL', 10);
15774
+ if (/\b(vec2f|vec3f|vec4f|mat4x4f|f32|u32|i32)\b/.test(text))
15775
+ add('WGSL', 12);
15776
+ // GLSL
15777
+ if (/\b(gl_Position|gl_FragColor|gl_FragCoord)\b/.test(text))
15778
+ add('GLSL', 20);
15779
+ if (/\b(uniform|varying|attribute)\s+\w/.test(text))
15780
+ add('GLSL', 10);
15781
+ if (/\b(vec2|vec3|vec4|mat4|sampler2D)\b/.test(text) && !/vec2f|vec3f/.test(text))
15782
+ add('GLSL', 8);
15783
+ // HLSL
15784
+ if (/\b(SV_Position|SV_Target|SV_Depth)\b/.test(text))
15785
+ add('HLSL', 20);
15786
+ if (/\b(cbuffer|tbuffer|float4|float3|float2|Texture2D)\b/.test(text))
15787
+ add('HLSL', 12);
15788
+ // Python
15789
+ if (/^\s*def\s+\w+\s*\(/m.test(text))
15790
+ add('Python', 15);
15791
+ if (/^\s*import\s+\w/m.test(text) && !/\bfrom\s+['"]/.test(text))
15792
+ add('Python', 8);
15793
+ if (/^\s*class\s+\w+(\s*\(.*\))?:/m.test(text))
15794
+ add('Python', 10);
15795
+ if (/\bprint\s*\(/.test(text) && /:\s*$/.test(text))
15796
+ add('Python', 5);
15797
+ if (/elif\b|lambda\b|self\.\w/.test(text))
15798
+ add('Python', 8);
15799
+ // Rust
15800
+ if (/\bfn\s+\w+\s*\(/.test(text) && /\blet\s+mut\b/.test(text))
15801
+ add('Rust', 15);
15802
+ if (/\b(impl|trait|enum|struct)\s+\w+/.test(text) && /\bfn\b/.test(text))
15803
+ add('Rust', 12);
15804
+ if (/use\s+std::|use\s+\w+::\w+/.test(text))
15805
+ add('Rust', 10);
15806
+ if (/println!\s*\(/.test(text))
15807
+ add('Rust', 8);
15808
+ // C++
15809
+ if (/#include\s*<[\w./]+>/.test(text))
15810
+ add('C++', 15);
15811
+ if (/\bstd::\w+/.test(text))
15812
+ add('C++', 12);
15813
+ if (/\b(class|template|namespace|nullptr|new\s+\w)\b/.test(text))
15814
+ add('C++', 8);
15815
+ if (/(::|->)\w+/.test(text))
15816
+ add('C++', 6);
15817
+ // C
15818
+ if (/#include\s*<[\w.]+\.h>/.test(text))
15819
+ add('C', 12);
15820
+ if (/\bint\s+main\s*\(/.test(text))
15821
+ add('C', 15);
15822
+ if (/\b(printf|scanf|malloc|free|sizeof)\s*\(/.test(text))
15823
+ add('C', 10);
15824
+ // TypeScript — before JS (is a superset)
15825
+ if (/:\s*(string|number|boolean|void|any|never|unknown)\b/.test(text))
15826
+ add('TypeScript', 12);
15827
+ if (/\binterface\s+\w+/.test(text))
15828
+ add('TypeScript', 12);
15829
+ if (/\btype\s+\w+\s*=/.test(text))
15830
+ add('TypeScript', 10);
15831
+ if (/\bas\s+(string|number|any|unknown)\b/.test(text))
15832
+ add('TypeScript', 8);
15833
+ if (/\benum\s+\w+\s*\{/.test(text))
15834
+ add('TypeScript', 8);
15835
+ // JavaScript
15836
+ if (/\b(const|let|var)\s+\w+\s*=/.test(text))
15837
+ add('JavaScript', 8);
15838
+ if (/=>\s*[\w{(]/.test(text))
15839
+ add('JavaScript', 6);
15840
+ if (/\b(import|export)\s+(default|{|\*)/.test(text))
15841
+ add('JavaScript', 8);
15842
+ if (/\bconsole\.(log|warn|error)\b/.test(text))
15843
+ add('JavaScript', 6);
15844
+ // Markdown
15845
+ if (/^#{1,6}\s+\S/m.test(text))
15846
+ add('Markdown', 12);
15847
+ if (/\*\*\w.*?\*\*/.test(text) || /\[.+\]\(.+\)/.test(text))
15848
+ add('Markdown', 8);
15849
+ if (/^```\w*/m.test(text))
15850
+ add('Markdown', 10);
15851
+ const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
15852
+ return best && best[1] >= 8 ? best[0] : null;
15853
+ }
15693
15854
  appendText(text) {
15694
15855
  const cursor = this.cursorSet.getPrimary();
15695
15856
  const { line, col } = cursor.head;
@@ -16188,8 +16349,8 @@ class CodeEditor {
16188
16349
  : Tokenizer.initialState();
16189
16350
  const lineText = this.doc.getLine(lineIndex);
16190
16351
  const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
16191
- // Build HTML
16192
16352
  const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
16353
+ const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
16193
16354
  let html = '';
16194
16355
  for (const token of result.tokens) {
16195
16356
  const cls = TOKEN_CLASS_MAP[token.type];
@@ -16197,11 +16358,15 @@ class CodeEditor {
16197
16358
  .replace(/&/g, '&amp;')
16198
16359
  .replace(/</g, '&lt;')
16199
16360
  .replace(/>/g, '&gt;');
16361
+ // Wrap URLs in comment tokens with a clickable span
16362
+ const content = (token.type === 'comment')
16363
+ ? escaped.replace(URL_REGEX, `<span class="code-link" data-url="$1">$1</span>`)
16364
+ : escaped;
16200
16365
  if (cls) {
16201
- html += `<span class="${cls} ${langClass}">${escaped}</span>`;
16366
+ html += `<span class="${cls} ${langClass}">${content}</span>`;
16202
16367
  }
16203
16368
  else {
16204
- html += escaped;
16369
+ html += content;
16205
16370
  }
16206
16371
  }
16207
16372
  return { html: html || '&nbsp;', endState: result.state, tokens: result.tokens };
@@ -17031,11 +17196,25 @@ class CodeEditor {
17031
17196
  const { line, col } = cursor.head;
17032
17197
  const indent = this.doc.getIndent(line);
17033
17198
  const spaces = ' '.repeat(indent);
17034
- const op = this.doc.insert(line, col, '\n' + spaces);
17035
- this.undoManager.record(op, this.cursorSet.getCursorPositions());
17036
- cursor.head = { line: line + 1, col: indent };
17037
- cursor.anchor = { ...cursor.head };
17038
- this.cursorSet.adjustOthers(idx, line, col, 0, 1);
17199
+ const charBefore = this.doc.getCharAt(line, col - 1);
17200
+ const charAfter = this.doc.getCharAt(line, col);
17201
+ const OPEN_CLOSE = { '{': '}', '[': ']', '(': ')' };
17202
+ if (charBefore && charAfter && OPEN_CLOSE[charBefore] === charAfter) {
17203
+ const innerSpaces = ' '.repeat(indent + this.tabSize);
17204
+ const insertion = '\n' + innerSpaces + '\n' + spaces;
17205
+ const op = this.doc.insert(line, col, insertion);
17206
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17207
+ cursor.head = { line: line + 1, col: indent + this.tabSize };
17208
+ cursor.anchor = { ...cursor.head };
17209
+ this.cursorSet.adjustOthers(idx, line, col, 0, 2);
17210
+ }
17211
+ else {
17212
+ const op = this.doc.insert(line, col, '\n' + spaces);
17213
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17214
+ cursor.head = { line: line + 1, col: indent };
17215
+ cursor.anchor = { ...cursor.head };
17216
+ this.cursorSet.adjustOthers(idx, line, col, 0, 1);
17217
+ }
17039
17218
  }
17040
17219
  this._rebuildLines();
17041
17220
  this._afterCursorMove();
@@ -17049,8 +17228,36 @@ class CodeEditor {
17049
17228
  for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
17050
17229
  const cursor = this.cursorSet.cursors[idx];
17051
17230
  const { line, col } = cursor.head;
17052
- if (shift) {
17053
- // Dedent: remove up to tabSize spaces from start
17231
+ const anchorLine = cursor.anchor.line;
17232
+ // Multiline selection: indent/dedent all lines in the selection
17233
+ const startLine = Math.min(line, anchorLine);
17234
+ const endLine = Math.max(line, anchorLine);
17235
+ const isMultiline = startLine !== endLine;
17236
+ if (isMultiline) {
17237
+ for (let i = startLine; i <= endLine; i++) {
17238
+ if (shift) {
17239
+ const lineText = this.doc.getLine(i);
17240
+ let spacesToRemove = 0;
17241
+ while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
17242
+ spacesToRemove++;
17243
+ }
17244
+ if (spacesToRemove > 0) {
17245
+ const op = this.doc.delete(i, 0, spacesToRemove);
17246
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17247
+ }
17248
+ }
17249
+ else {
17250
+ const spaces = ' '.repeat(this.tabSize);
17251
+ const op = this.doc.insert(i, 0, spaces);
17252
+ this.undoManager.record(op, this.cursorSet.getCursorPositions());
17253
+ }
17254
+ }
17255
+ const delta = shift ? -this.tabSize : this.tabSize;
17256
+ cursor.head = { line, col: Math.max(0, col + delta) };
17257
+ cursor.anchor = { line: anchorLine, col: Math.max(0, cursor.anchor.col + delta) };
17258
+ }
17259
+ else if (shift) {
17260
+ // Single line dedent: remove up to tabSize spaces from start
17054
17261
  const lineText = this.doc.getLine(line);
17055
17262
  let spacesToRemove = 0;
17056
17263
  while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
@@ -17065,6 +17272,7 @@ class CodeEditor {
17065
17272
  }
17066
17273
  }
17067
17274
  else {
17275
+ // Single line indent: insert spaces at cursor
17068
17276
  const spacesToAdd = this.tabSize - (col % this.tabSize);
17069
17277
  const spaces = ' '.repeat(spacesToAdd);
17070
17278
  const op = this.doc.insert(line, col, spaces);
@@ -17098,8 +17306,10 @@ class CodeEditor {
17098
17306
  this.cursorSet.adjustOthers(idx, start.line, start.col, -colDelta, -linesRemoved);
17099
17307
  anyDeleted = true;
17100
17308
  }
17101
- if (anyDeleted)
17309
+ if (anyDeleted) {
17102
17310
  this._rebuildLines();
17311
+ this._doHideAutocomplete();
17312
+ }
17103
17313
  }
17104
17314
  // Clipboard helpers:
17105
17315
  _doCopy() {
@@ -17213,6 +17423,15 @@ class CodeEditor {
17213
17423
  return;
17214
17424
  if (this.autocomplete && this.autocomplete.contains(e.target))
17215
17425
  return;
17426
+ // Ctrl+click: open link if cursor is over a code-link span
17427
+ if (e.ctrlKey && e.button === 0) {
17428
+ const target = e.target;
17429
+ const link = target.closest('.code-link');
17430
+ if (link?.dataset.url) {
17431
+ window.open(link.dataset.url, '_blank');
17432
+ return;
17433
+ }
17434
+ }
17216
17435
  e.preventDefault(); // Prevent browser from stealing focus from _inputArea
17217
17436
  this._wasPaired = false;
17218
17437
  // Calculate line and column from click position
@@ -17330,9 +17549,9 @@ class CodeEditor {
17330
17549
  }
17331
17550
  const suggestions = [];
17332
17551
  const added = new Set();
17333
- const addSuggestion = (label, kind, scope, detail) => {
17552
+ const addSuggestion = (label, kind, scope, detail, insertText) => {
17334
17553
  if (!added.has(label)) {
17335
- suggestions.push({ label, kind, scope, detail });
17554
+ suggestions.push({ label, kind, scope, detail, insertText });
17336
17555
  added.add(label);
17337
17556
  }
17338
17557
  };
@@ -17354,8 +17573,9 @@ class CodeEditor {
17354
17573
  const label = typeof suggestion === 'string' ? suggestion : suggestion.label;
17355
17574
  const kind = typeof suggestion === 'object' ? suggestion.kind : undefined;
17356
17575
  const detail = typeof suggestion === 'object' ? suggestion.detail : undefined;
17576
+ const insertText = typeof suggestion === 'object' ? suggestion.insertText : suggestion;
17357
17577
  if (label.toLowerCase().startsWith(word.toLowerCase())) {
17358
- addSuggestion(label, kind, undefined, detail);
17578
+ addSuggestion(label, kind, undefined, detail, insertText);
17359
17579
  }
17360
17580
  }
17361
17581
  // Close autocomplete if no suggestions
@@ -17375,6 +17595,7 @@ class CodeEditor {
17375
17595
  // Render suggestions
17376
17596
  suggestions.forEach((suggestion, index) => {
17377
17597
  const item = document.createElement('pre');
17598
+ item.insertText = suggestion.insertText ?? suggestion.label;
17378
17599
  if (index === this._selectedAutocompleteIndex)
17379
17600
  item.classList.add('selected');
17380
17601
  const currSuggestion = suggestion.label;
@@ -17493,8 +17714,8 @@ class CodeEditor {
17493
17714
  * Insert the selected autocomplete word at cursor.
17494
17715
  */
17495
17716
  _doAutocompleteWord() {
17496
- const word = this._getSelectedAutoCompleteWord();
17497
- if (!word)
17717
+ const text = this._getSelectedAutoCompleteWord();
17718
+ if (!text)
17498
17719
  return;
17499
17720
  const cursor = this.cursorSet.getPrimary().head;
17500
17721
  const { start, end } = this._getWordAtCursor();
@@ -17504,8 +17725,14 @@ class CodeEditor {
17504
17725
  const deleteOp = this.doc.delete(line, start, end - start);
17505
17726
  this.undoManager.record(deleteOp, cursorsBefore);
17506
17727
  }
17507
- const insertOp = this.doc.insert(line, start, word);
17508
- this.cursorSet.set(line, start + word.length);
17728
+ const insertOp = this.doc.insert(line, start, text);
17729
+ const insertedLines = text.split(/\r?\n/);
17730
+ if (insertedLines.length === 1) {
17731
+ this.cursorSet.set(line, start + text.length);
17732
+ }
17733
+ else {
17734
+ this.cursorSet.set(line + insertedLines.length - 1, insertedLines[insertedLines.length - 1].length);
17735
+ }
17509
17736
  const cursorsAfter = this.cursorSet.getCursorPositions();
17510
17737
  this.undoManager.record(insertOp, cursorsAfter);
17511
17738
  this._rebuildLines();
@@ -17516,15 +17743,7 @@ class CodeEditor {
17516
17743
  if (!this.autocomplete || !this._isAutoCompleteActive)
17517
17744
  return null;
17518
17745
  const pre = this.autocomplete.childNodes[this._selectedAutocompleteIndex];
17519
- var word = '';
17520
- for (let childSpan of pre.childNodes) {
17521
- const span = childSpan;
17522
- if (span.constructor != HTMLSpanElement || span.classList.contains('kind')) {
17523
- continue;
17524
- }
17525
- word += span.textContent;
17526
- }
17527
- return word;
17746
+ return pre.insertText;
17528
17747
  }
17529
17748
  _afterCursorMove() {
17530
17749
  this._renderCursors();
@@ -17557,7 +17776,8 @@ class CodeEditor {
17557
17776
  _resetGutter() {
17558
17777
  // Use cached value or compute if not available (e.g., on initial load)
17559
17778
  const tabsHeight = this._cachedTabsHeight || (this.tabs?.root.getBoundingClientRect().height ?? 0);
17560
- this.lineGutter.style.height = `calc(100% - ${tabsHeight}px)`;
17779
+ const statusPanelHeight = this._cachedStatusPanelHeight || (this.statusPanel?.root.getBoundingClientRect().height ?? 0);
17780
+ this.lineGutter.style.height = `calc(100% - ${tabsHeight + statusPanelHeight}px)`;
17561
17781
  }
17562
17782
  getMaxLineLength() {
17563
17783
  if (!this.currentTab)
@@ -27585,8 +27805,7 @@ class VideoEditor {
27585
27805
  }
27586
27806
  else {
27587
27807
  [videoArea, controlsArea] = area.split({ type: 'vertical',
27588
- sizes: [controlsOptions.height ? `calc(100% - ${controlsOptions.height})` : '85%', null], minimizable: false,
27589
- resize: false });
27808
+ sizes: [controlsOptions.height ? `calc(100% - ${controlsOptions.height})` : '85%', null], minimizable: false, resize: false });
27590
27809
  }
27591
27810
  controlsArea.root.classList.add('lexconstrolsarea');
27592
27811
  this.cropArea = document.createElement('div');