lexgui 8.3.2 → 8.4.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.
@@ -48,7 +48,7 @@ declare class CodeDocument {
48
48
  constructor(onChange?: (doc: CodeDocument) => void);
49
49
  getLine(n: number): string;
50
50
  getText(separator?: string): string;
51
- setText(text: string): void;
51
+ setText(text: string, silent?: boolean): void;
52
52
  getCharAt(line: number, col: number): string | undefined;
53
53
  /**
54
54
  * Get the word at a given position. Returns [word, startCol, endCol].
@@ -103,6 +103,7 @@ declare class UndoManager {
103
103
  private _lastPushTime;
104
104
  private _groupThresholdMs;
105
105
  private _maxSteps;
106
+ private _savedDepth;
106
107
  constructor(groupThresholdMs?: number, maxSteps?: number);
107
108
  /**
108
109
  * Record an edit operation. Consecutive operations within the time threshold
@@ -127,6 +128,8 @@ declare class UndoManager {
127
128
  } | null;
128
129
  canUndo(): boolean;
129
130
  canRedo(): boolean;
131
+ markSaved(): void;
132
+ isModified(): boolean;
130
133
  clear(): void;
131
134
  private _flush;
132
135
  }
@@ -230,6 +233,7 @@ interface CodeTab {
230
233
  cursorSet: CursorSet;
231
234
  undoManager: UndoManager;
232
235
  language: string;
236
+ modified: boolean;
233
237
  title?: string;
234
238
  path?: string;
235
239
  }
@@ -243,6 +247,8 @@ export interface CodeSuggestion {
243
247
  sortText?: string;
244
248
  icon?: string;
245
249
  iconClass?: string;
250
+ cursorOffset?: number;
251
+ selectLength?: number;
246
252
  }
247
253
  export interface HoverSymbolInfo {
248
254
  word: string;
@@ -326,6 +332,7 @@ export declare class CodeEditor {
326
332
  onReady: ((editor: CodeEditor) => void) | undefined;
327
333
  onCreateFile: ((editor: CodeEditor) => void) | undefined;
328
334
  onCodeChange: ((doc: CodeDocument) => void) | undefined;
335
+ onOpenPath: ((path: string, editor: CodeEditor) => void) | undefined;
329
336
  onHoverSymbol: ((info: HoverSymbolInfo, editor: CodeEditor) => string | HTMLElement | null | undefined) | undefined;
330
337
  private _inputArea;
331
338
  private _lineStates;
@@ -387,6 +394,8 @@ export declare class CodeEditor {
387
394
  loadFile(file: File | string, options?: Record<string, any>): void;
388
395
  loadFiles(files: string[], onComplete?: (editor: CodeEditor, results: any[], total: number) => void, async?: boolean): Promise<void>;
389
396
  _setupEditorWhenVisible(): Promise<void>;
397
+ private _findTabByPath;
398
+ private _setTabModified;
390
399
  private _onSelectTab;
391
400
  private _onNewTab;
392
401
  private _onCreateNewFile;
@@ -481,7 +490,7 @@ export declare class CodeEditor {
481
490
  * Insert the selected autocomplete word at cursor.
482
491
  */
483
492
  private _doAutocompleteWord;
484
- private _getSelectedAutoCompleteWord;
493
+ private _getSelectedAutoCompleteSuggestion;
485
494
  private _afterCursorMove;
486
495
  /**
487
496
  * Returns the scope stack at the exact cursor position (line + column).
@@ -955,12 +955,12 @@ class CodeDocument {
955
955
  getText(separator = '\n') {
956
956
  return this._lines.join(separator);
957
957
  }
958
- setText(text) {
958
+ setText(text, silent = false) {
959
959
  this._lines = text.split(/\r?\n/);
960
960
  if (this._lines.length === 0) {
961
961
  this._lines = [''];
962
962
  }
963
- if (this.onChange)
963
+ if (!silent && this.onChange)
964
964
  this.onChange(this);
965
965
  }
966
966
  getCharAt(line, col) {
@@ -1124,6 +1124,7 @@ class UndoManager {
1124
1124
  _lastPushTime = 0;
1125
1125
  _groupThresholdMs;
1126
1126
  _maxSteps;
1127
+ _savedDepth = 0;
1127
1128
  constructor(groupThresholdMs = 2000, maxSteps = 200) {
1128
1129
  this._groupThresholdMs = groupThresholdMs;
1129
1130
  this._maxSteps = maxSteps;
@@ -1195,11 +1196,19 @@ class UndoManager {
1195
1196
  canRedo() {
1196
1197
  return this._redoStack.length > 0;
1197
1198
  }
1199
+ markSaved() {
1200
+ this._flush();
1201
+ this._savedDepth = this._undoStack.length;
1202
+ }
1203
+ isModified() {
1204
+ return this._undoStack.length !== this._savedDepth || this._pendingOps.length > 0;
1205
+ }
1198
1206
  clear() {
1199
1207
  this._undoStack.length = 0;
1200
1208
  this._redoStack.length = 0;
1201
1209
  this._pendingOps.length = 0;
1202
1210
  this._lastPushTime = 0;
1211
+ this._savedDepth = 0;
1203
1212
  }
1204
1213
  _flush() {
1205
1214
  if (this._pendingOps.length === 0)
@@ -1768,8 +1777,36 @@ LX.Area;
1768
1777
  LX.Panel;
1769
1778
  LX.Tabs;
1770
1779
  LX.NodeTree;
1771
- /** Matches hex color literals: #rgb #rgba #rrggbb #rrggbbaa */
1772
1780
  const HEX_COLOR_RE = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g;
1781
+ const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
1782
+ /**
1783
+ * Returns true if the string token at `idx` in the token list is a module import path.
1784
+ */
1785
+ function isImportPath(tokens, idx) {
1786
+ const isWs = (t) => /^\s+$/.test(t.value);
1787
+ const isImportWord = (t) => t.value === 'require' || t.value === 'import';
1788
+ for (let i = idx - 1; i >= 0; i--) {
1789
+ const t = tokens[i];
1790
+ if (isWs(t))
1791
+ continue;
1792
+ if (t.type === 'keyword' && t.value === 'from')
1793
+ return true;
1794
+ if (isImportWord(t))
1795
+ return true;
1796
+ if (t.type === 'symbol' && t.value === '(') {
1797
+ for (let j = i - 1; j >= 0; j--) {
1798
+ const t2 = tokens[j];
1799
+ if (isWs(t2))
1800
+ continue;
1801
+ if (isImportWord(t2))
1802
+ return true;
1803
+ break;
1804
+ }
1805
+ }
1806
+ break;
1807
+ }
1808
+ return false;
1809
+ }
1773
1810
  /**
1774
1811
  * Scans a raw token value for hex color literals and returns HTML with each
1775
1812
  * color wrapped in a swatch span. Non-color text is HTML-escaped.
@@ -1812,6 +1849,15 @@ class ScrollBar {
1812
1849
  this.thumb = LX.makeElement('div');
1813
1850
  this.thumb.addEventListener('mousedown', (e) => this._onMouseDown(e));
1814
1851
  this.root.appendChild(this.thumb);
1852
+ this.root.addEventListener('mousedown', (e) => {
1853
+ if (e.target === this.thumb)
1854
+ return;
1855
+ const clickPos = this._vertical ? e.offsetY : e.offsetX;
1856
+ const thumbSize = this._vertical ? this.thumb.offsetHeight : this.thumb.offsetWidth;
1857
+ const delta = (clickPos - thumbSize / 2) - this._thumbPos;
1858
+ this._onDrag?.(delta);
1859
+ this._onMouseDown(e); // continue as drag from new position
1860
+ });
1815
1861
  }
1816
1862
  setThumbRatio(ratio) {
1817
1863
  this._thumbRatio = LX.clamp(ratio, 0, 1);
@@ -1935,6 +1981,7 @@ class CodeEditor {
1935
1981
  onReady;
1936
1982
  onCreateFile;
1937
1983
  onCodeChange;
1984
+ onOpenPath;
1938
1985
  onHoverSymbol;
1939
1986
  _inputArea;
1940
1987
  // State:
@@ -2015,6 +2062,7 @@ class CodeEditor {
2015
2062
  this.onSelectTab = options.onSelectTab;
2016
2063
  this.onReady = options.onReady;
2017
2064
  this.onCodeChange = options.onCodeChange;
2065
+ this.onOpenPath = options.onOpenPath;
2018
2066
  this.onHoverSymbol = options.onHoverSymbol;
2019
2067
  this.language = Tokenizer.getLanguage(this.highlight) ?? Tokenizer.getLanguage('Plain Text');
2020
2068
  this.symbolTable = new SymbolTable();
@@ -2225,19 +2273,31 @@ class CodeEditor {
2225
2273
  this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
2226
2274
  this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
2227
2275
  this.codeArea.root.addEventListener('mouseover', (e) => {
2228
- const link = e.target.closest('.code-link');
2276
+ const target = e.target;
2277
+ const link = target.closest('.code-link');
2229
2278
  if (link && e.ctrlKey)
2230
2279
  link.classList.add('hovered');
2280
+ const path = target.closest('.code-path');
2281
+ if (path && e.ctrlKey)
2282
+ path.classList.add('hovered');
2231
2283
  });
2232
2284
  this.codeArea.root.addEventListener('mouseout', (e) => {
2233
- const link = e.target.closest('.code-link');
2285
+ const target = e.target;
2286
+ const link = target.closest('.code-link');
2234
2287
  if (link)
2235
2288
  link.classList.remove('hovered');
2289
+ const path = target.closest('.code-path');
2290
+ if (path)
2291
+ path.classList.remove('hovered');
2236
2292
  });
2237
2293
  this.codeArea.root.addEventListener('mousemove', (e) => {
2238
- const link = e.target.closest('.code-link');
2294
+ const target = e.target;
2295
+ const link = target.closest('.code-link');
2239
2296
  if (link)
2240
2297
  link.classList.toggle('hovered', e.ctrlKey);
2298
+ const path = target.closest('.code-path');
2299
+ if (path)
2300
+ path.classList.toggle('hovered', e.ctrlKey);
2241
2301
  this._onCodeAreaMouseMove(e);
2242
2302
  });
2243
2303
  this.codeArea.root.addEventListener('mouseleave', () => {
@@ -2336,7 +2396,7 @@ class CodeEditor {
2336
2396
  setText(text, language, detectLang = false) {
2337
2397
  if (!this.currentTab)
2338
2398
  return;
2339
- this.doc.setText(this._normalizeText(text));
2399
+ this.doc.setText(this._normalizeText(text), true);
2340
2400
  this.cursorSet.set(0, 0);
2341
2401
  this.undoManager.clear();
2342
2402
  this._lineStates = [];
@@ -2527,10 +2587,14 @@ class CodeEditor {
2527
2587
  const codeTab = {
2528
2588
  name,
2529
2589
  dom,
2530
- doc: new CodeDocument(this.onCodeChange),
2590
+ doc: new CodeDocument((doc) => {
2591
+ this._setTabModified(name, true);
2592
+ this.onCodeChange?.(doc);
2593
+ }),
2531
2594
  cursorSet: new CursorSet(),
2532
2595
  undoManager: new UndoManager(),
2533
2596
  language: langName,
2597
+ modified: false,
2534
2598
  title: options.title ?? name
2535
2599
  };
2536
2600
  this._openedTabs[name] = codeTab;
@@ -2554,7 +2618,7 @@ class CodeEditor {
2554
2618
  // Move into the sizer..
2555
2619
  this.codeSizer.appendChild(dom);
2556
2620
  if (options.text) {
2557
- codeTab.doc.setText(options.text);
2621
+ codeTab.doc.setText(options.text, true);
2558
2622
  codeTab.cursorSet.set(0, 0);
2559
2623
  codeTab.undoManager.clear();
2560
2624
  this._renderAllLines();
@@ -2645,7 +2709,7 @@ class CodeEditor {
2645
2709
  title: options.title ?? name,
2646
2710
  language: langName
2647
2711
  });
2648
- this.doc.setText(text);
2712
+ this.doc.setText(text, true);
2649
2713
  this.setLanguage(langName, ext);
2650
2714
  this.cursorSet.set(0, 0);
2651
2715
  this.undoManager.clear();
@@ -2706,7 +2770,7 @@ class CodeEditor {
2706
2770
  language: langName
2707
2771
  });
2708
2772
  if (results.length === 0) {
2709
- this.doc.setText(processedText);
2773
+ this.doc.setText(processedText, true);
2710
2774
  this.setLanguage(langName, ext);
2711
2775
  this.cursorSet.set(0, 0);
2712
2776
  this.undoManager.clear();
@@ -2748,6 +2812,30 @@ class CodeEditor {
2748
2812
  }
2749
2813
  }, 20);
2750
2814
  }
2815
+ _findTabByPath(importPath) {
2816
+ // By now only uses base name
2817
+ const importBase = importPath.split('/').pop().replace(/\.\w+$/, '').toLowerCase();
2818
+ const allNames = new Set([
2819
+ ...Object.keys(this._openedTabs),
2820
+ ...Object.keys(this._loadedTabs),
2821
+ ...Object.keys(this._storedTabs),
2822
+ ]);
2823
+ for (const name of allNames) {
2824
+ const tabBase = name.split('/').pop().replace(/\.\w+$/, '').toLowerCase();
2825
+ if (tabBase === importBase)
2826
+ return name;
2827
+ }
2828
+ return null;
2829
+ }
2830
+ _setTabModified(name, modified) {
2831
+ const tab = this._openedTabs[name];
2832
+ if (!tab || tab.modified === modified)
2833
+ return;
2834
+ tab.modified = modified;
2835
+ const tabEl = this.tabs?.tabDOMs?.[name];
2836
+ if (tabEl)
2837
+ tabEl.toggleAttribute('data-modified', modified);
2838
+ }
2751
2839
  _onSelectTab(isNewTabButton, event, name) {
2752
2840
  if (this.disableEdition) {
2753
2841
  return;
@@ -2973,7 +3061,6 @@ class CodeEditor {
2973
3061
  const lineText = this.doc.getLine(lineIndex);
2974
3062
  const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
2975
3063
  const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
2976
- const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
2977
3064
  // Pre-compute which token index gets the bracket-highlight class
2978
3065
  let bracketTokenIdx = -1;
2979
3066
  if (lineIndex === this._bracketOpenLine) {
@@ -3001,15 +3088,20 @@ class CodeEditor {
3001
3088
  const cls = TOKEN_CLASS_MAP[token.type];
3002
3089
  const tokenCol = colOffset;
3003
3090
  colOffset += token.value.length;
3091
+ // Inject content depending on type of token: color, url, path?
3004
3092
  let content;
3005
3093
  if (token.type === 'comment') {
3006
- // Escape then inject clickable URL spans
3007
3094
  const escaped = token.value
3008
3095
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3009
3096
  content = escaped.replace(URL_REGEX, `<span class="code-link" data-url="$1">$1</span>`);
3010
3097
  }
3098
+ else if (token.type === 'string' && isImportPath(result.tokens, ti)) {
3099
+ const inner = token.value.slice(1, -1); // strip surrounding quotes
3100
+ const q = token.value[0];
3101
+ const escapedInner = inner.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3102
+ content = `${q}<span class="code-path" data-path="${inner}">${escapedInner}</span>${q}`;
3103
+ }
3011
3104
  else {
3012
- // Escape and inject color swatches for hex color strings
3013
3105
  content = injectColorSpans(token.value, lineIndex, tokenCol);
3014
3106
  }
3015
3107
  const bracketClass = ti === bracketTokenIdx ? ' code-bracket-active' : '';
@@ -3279,6 +3371,8 @@ class CodeEditor {
3279
3371
  e.preventDefault();
3280
3372
  if (this.onSave) {
3281
3373
  this.onSave(this.getText(), this);
3374
+ this.undoManager.markSaved();
3375
+ this._setTabModified(this.currentTab.name, false);
3282
3376
  }
3283
3377
  return;
3284
3378
  case 'z':
@@ -3301,6 +3395,17 @@ class CodeEditor {
3301
3395
  e.preventDefault();
3302
3396
  this._doPaste();
3303
3397
  return;
3398
+ case 'home':
3399
+ e.preventDefault();
3400
+ this.cursorSet.set(0, 0);
3401
+ this._afterCursorMove();
3402
+ return;
3403
+ case 'end':
3404
+ e.preventDefault();
3405
+ const lastLine = this.doc.lineCount - 1;
3406
+ this.cursorSet.set(lastLine, this.doc.getLine(lastLine).length);
3407
+ this._afterCursorMove();
3408
+ return;
3304
3409
  case ' ':
3305
3410
  e.preventDefault();
3306
3411
  // Also call user callback if provided
@@ -4073,6 +4178,8 @@ class CodeEditor {
4073
4178
  }
4074
4179
  this._rebuildLines();
4075
4180
  this._afterCursorMove();
4181
+ if (this.currentTab)
4182
+ this._setTabModified(this.currentTab.name, this.undoManager.isModified());
4076
4183
  }
4077
4184
  }
4078
4185
  _doRedo() {
@@ -4084,6 +4191,8 @@ class CodeEditor {
4084
4191
  }
4085
4192
  this._rebuildLines();
4086
4193
  this._afterCursorMove();
4194
+ if (this.currentTab)
4195
+ this._setTabModified(this.currentTab.name, this.undoManager.isModified());
4087
4196
  }
4088
4197
  }
4089
4198
  // Mouse input events:
@@ -4094,7 +4203,7 @@ class CodeEditor {
4094
4203
  return;
4095
4204
  if (this.autocomplete && this.autocomplete.contains(e.target))
4096
4205
  return;
4097
- // Ctrl+click: open link if cursor is over a code-link span
4206
+ // Ctrl+click: open link or import path
4098
4207
  if (e.ctrlKey && e.button === 0) {
4099
4208
  const target = e.target;
4100
4209
  const link = target.closest('.code-link');
@@ -4102,6 +4211,15 @@ class CodeEditor {
4102
4211
  window.open(link.dataset.url, '_blank');
4103
4212
  return;
4104
4213
  }
4214
+ const pathEl = target.closest('.code-path');
4215
+ if (pathEl?.dataset.path) {
4216
+ const rawPath = pathEl.dataset.path;
4217
+ const tabName = this._findTabByPath(rawPath);
4218
+ if (tabName)
4219
+ this.loadTab(tabName);
4220
+ this.onOpenPath?.(rawPath, this);
4221
+ return;
4222
+ }
4105
4223
  }
4106
4224
  e.preventDefault(); // Prevent browser from stealing focus from _inputArea
4107
4225
  this._wasPaired = false;
@@ -4149,18 +4267,55 @@ class CodeEditor {
4149
4267
  }
4150
4268
  this._afterCursorMove();
4151
4269
  this._inputArea.focus();
4152
- // Track mouse for drag selection
4153
- const onMouseMove = (me) => {
4154
- const mx = me.clientX - rect.left - this.xPadding;
4155
- const my = me.clientY - rect.top;
4156
- const ml = Math.max(0, Math.min(Math.floor(my / this.lineHeight), this.doc.lineCount - 1));
4157
- const mc = Math.max(0, Math.min(Math.round(mx / this.charWidth), this.doc.getLine(ml).length));
4270
+ // Track mouse for drag selection (with auto-scroll when outside editor window/area)
4271
+ let lastMouseX = 0;
4272
+ let lastMouseY = 0;
4273
+ let rafId = null;
4274
+ const updateSelection = () => {
4275
+ const currentRect = this.codeContainer.getBoundingClientRect();
4276
+ const mx = lastMouseX - currentRect.left - this.xPadding;
4277
+ const my = lastMouseY - currentRect.top;
4278
+ const ml = LX.clamp(Math.floor(my / this.lineHeight), 0, this.doc.lineCount - 1);
4279
+ const mc = LX.clamp(Math.round(mx / this.charWidth), 0, this.doc.getLine(ml).length);
4158
4280
  const sel = this.cursorSet.getPrimary();
4159
4281
  sel.head = { line: ml, col: mc };
4160
4282
  this._renderCursors();
4161
4283
  this._renderSelections();
4162
4284
  };
4285
+ const autoScroll = () => {
4286
+ const scrollerRect = this.codeScroller.getBoundingClientRect();
4287
+ const overshootY = lastMouseY < scrollerRect.top ? lastMouseY - scrollerRect.top
4288
+ : lastMouseY > scrollerRect.bottom ? lastMouseY - scrollerRect.bottom : 0;
4289
+ const overshootX = lastMouseX < scrollerRect.left ? lastMouseX - scrollerRect.left
4290
+ : lastMouseX > scrollerRect.right ? lastMouseX - scrollerRect.right : 0;
4291
+ if (overshootY === 0 && overshootX === 0) {
4292
+ rafId = null;
4293
+ return;
4294
+ }
4295
+ const speedY = Math.sign(overshootY) * Math.min(Math.abs(overshootY) * 0.3, 15);
4296
+ const speedX = Math.sign(overshootX) * Math.min(Math.abs(overshootX) * 0.3, 15);
4297
+ this.codeScroller.scrollTop += speedY;
4298
+ this.codeScroller.scrollLeft += speedX;
4299
+ this._syncScrollBars();
4300
+ updateSelection();
4301
+ rafId = requestAnimationFrame(autoScroll);
4302
+ };
4303
+ const onMouseMove = (me) => {
4304
+ lastMouseX = me.clientX;
4305
+ lastMouseY = me.clientY;
4306
+ updateSelection();
4307
+ const scrollerRect = this.codeScroller.getBoundingClientRect();
4308
+ const isOutside = me.clientY < scrollerRect.top || me.clientY > scrollerRect.bottom
4309
+ || me.clientX < scrollerRect.left || me.clientX > scrollerRect.right;
4310
+ if (isOutside && rafId === null) {
4311
+ rafId = requestAnimationFrame(autoScroll);
4312
+ }
4313
+ };
4163
4314
  const onMouseUp = () => {
4315
+ if (rafId !== null) {
4316
+ cancelAnimationFrame(rafId);
4317
+ rafId = null;
4318
+ }
4164
4319
  document.removeEventListener('mousemove', onMouseMove);
4165
4320
  document.removeEventListener('mouseup', onMouseUp);
4166
4321
  };
@@ -4221,7 +4376,10 @@ class CodeEditor {
4221
4376
  const suggestions = [];
4222
4377
  const added = new Set();
4223
4378
  const addSuggestion = (s) => {
4224
- if (!added.has(s.label)) {
4379
+ if (added.has(s.label)) {
4380
+ suggestions[suggestions.findIndex(x => x.label === s.label)] = s;
4381
+ }
4382
+ else {
4225
4383
  suggestions.push(s);
4226
4384
  added.add(s.label);
4227
4385
  }
@@ -4234,9 +4392,12 @@ class CodeEditor {
4234
4392
  return suggestion.label.toLowerCase().startsWith(w);
4235
4393
  };
4236
4394
  // Get first suggestions from symbol table
4395
+ const _skipKinds = new Set(['constructor-call', 'method-call']);
4237
4396
  const allSymbols = this.symbolTable.getAllSymbols();
4238
4397
  for (const symbol of allSymbols) {
4239
- const s = { label: symbol.name, kind: symbol.kind, scope: symbol.scope, detail: `${symbol.kind} in ${symbol.scope}` };
4398
+ if (_skipKinds.has(symbol.kind))
4399
+ continue;
4400
+ const s = { label: symbol.name, kind: symbol.kind, scope: symbol.scope };
4240
4401
  if (filterSuggestion(s, word))
4241
4402
  addSuggestion(s);
4242
4403
  }
@@ -4270,7 +4431,7 @@ class CodeEditor {
4270
4431
  // Render suggestions
4271
4432
  suggestions.forEach((suggestion, index) => {
4272
4433
  const item = document.createElement('pre');
4273
- item.insertText = suggestion.insertText ?? suggestion.label;
4434
+ item.suggestionData = suggestion;
4274
4435
  if (index === this._selectedAutocompleteIndex)
4275
4436
  item.classList.add('selected');
4276
4437
  const currSuggestionLabel = suggestion.label;
@@ -4299,11 +4460,15 @@ class CodeEditor {
4299
4460
  break;
4300
4461
  case 'type':
4301
4462
  iconName = 'Type';
4302
- iconClass = 'text-teal-500';
4463
+ iconClass = 'text-purple-500';
4303
4464
  break;
4304
4465
  case 'function':
4305
4466
  iconName = 'Function';
4306
- iconClass = 'text-purple-500';
4467
+ iconClass = 'text-teal-500';
4468
+ break;
4469
+ case 'constant':
4470
+ iconName = 'Pi';
4471
+ iconClass = 'text-rose-600';
4307
4472
  break;
4308
4473
  case 'method':
4309
4474
  iconName = 'Box';
@@ -4317,14 +4482,6 @@ class CodeEditor {
4317
4482
  iconName = 'Layers';
4318
4483
  iconClass = 'text-blue-300';
4319
4484
  break;
4320
- case 'constructor-call':
4321
- iconName = 'Hammer';
4322
- iconClass = 'text-green-500';
4323
- break;
4324
- case 'method-call':
4325
- iconName = 'Parentheses';
4326
- iconClass = 'text-gray-400';
4327
- break;
4328
4485
  }
4329
4486
  item.appendChild(LX.makeIcon(iconName, { iconClass: 'ml-1 mr-2', svgClass: 'sm ' + iconClass }));
4330
4487
  // Highlight the written part
@@ -4339,7 +4496,13 @@ class CodeEditor {
4339
4496
  var postWord = document.createElement('span');
4340
4497
  postWord.textContent = currSuggestionLabel.substring(hIndex + word.length);
4341
4498
  item.appendChild(postWord);
4342
- if (suggestion.kind) {
4499
+ if (suggestion.detail) {
4500
+ const detail = document.createElement('span');
4501
+ detail.textContent = ` ${suggestion.detail}`;
4502
+ detail.className = 'kind text-muted-foreground text-xs! ml-2';
4503
+ item.appendChild(detail);
4504
+ }
4505
+ else if (suggestion.kind) {
4343
4506
  const kind = document.createElement('span');
4344
4507
  kind.textContent = ` (${suggestion.kind})`;
4345
4508
  kind.className = 'kind text-muted-foreground text-xs! ml-2';
@@ -4385,9 +4548,12 @@ class CodeEditor {
4385
4548
  * Insert the selected autocomplete word at cursor.
4386
4549
  */
4387
4550
  _doAutocompleteWord() {
4388
- const text = this._getSelectedAutoCompleteWord();
4389
- if (!text)
4551
+ const suggestion = this._getSelectedAutoCompleteSuggestion();
4552
+ if (!suggestion)
4390
4553
  return;
4554
+ const text = suggestion.insertText ?? suggestion.label;
4555
+ const cursorOffset = suggestion.cursorOffset; // only valid in single line autocomplete
4556
+ const selectLength = suggestion.selectLength;
4391
4557
  const cursor = this.cursorSet.getPrimary().head;
4392
4558
  const { start, end } = this._getWordAtCursor();
4393
4559
  const line = cursor.line;
@@ -4399,7 +4565,13 @@ class CodeEditor {
4399
4565
  const insertOp = this.doc.insert(line, start, text);
4400
4566
  const insertedLines = text.split(/\r?\n/);
4401
4567
  if (insertedLines.length === 1) {
4402
- this.cursorSet.set(line, start + text.length);
4568
+ const cursorCol = start + (cursorOffset ?? text.length);
4569
+ this.cursorSet.set(line, cursorCol);
4570
+ if (selectLength) {
4571
+ const sel = this.cursorSet.getPrimary();
4572
+ sel.anchor = { line, col: cursorCol };
4573
+ sel.head = { line, col: cursorCol + selectLength };
4574
+ }
4403
4575
  }
4404
4576
  else {
4405
4577
  this.cursorSet.set(line + insertedLines.length - 1, insertedLines[insertedLines.length - 1].length);
@@ -4410,11 +4582,11 @@ class CodeEditor {
4410
4582
  this._afterCursorMove();
4411
4583
  this._doHideAutocomplete();
4412
4584
  }
4413
- _getSelectedAutoCompleteWord() {
4585
+ _getSelectedAutoCompleteSuggestion() {
4414
4586
  if (!this.autocomplete || !this._isAutoCompleteActive)
4415
4587
  return null;
4416
4588
  const pre = this.autocomplete.childNodes[this._selectedAutocompleteIndex];
4417
- return pre.insertText;
4589
+ return pre.suggestionData;
4418
4590
  }
4419
4591
  _afterCursorMove() {
4420
4592
  this._renderCursors();
@@ -4655,6 +4827,8 @@ class CodeEditor {
4655
4827
  this._hoverWord = '';
4656
4828
  }
4657
4829
  _onCodeAreaMouseMove(e) {
4830
+ if (!this.currentTab)
4831
+ return;
4658
4832
  // Only show hover when no button is pressed (no dragging)
4659
4833
  if (e.buttons !== 0) {
4660
4834
  this._clearHoverPopup();