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.
- package/build/components/BaseComponent.d.ts +4 -2
- package/build/components/Empty.d.ts +8 -0
- package/build/core/Namespace.js +1 -1
- package/build/core/Namespace.js.map +1 -1
- package/build/core/Panel.d.ts +14 -7
- package/build/extensions/AssetView.d.ts +3 -2
- package/build/extensions/AssetView.js +37 -19
- package/build/extensions/AssetView.js.map +1 -1
- package/build/extensions/CodeEditor.d.ts +11 -2
- package/build/extensions/CodeEditor.js +214 -40
- package/build/extensions/CodeEditor.js.map +1 -1
- package/build/lexgui.all.js +420 -77
- package/build/lexgui.all.js.map +1 -1
- package/build/lexgui.all.min.js +1 -1
- package/build/lexgui.all.module.js +420 -77
- package/build/lexgui.all.module.js.map +1 -1
- package/build/lexgui.all.module.min.js +1 -1
- package/build/lexgui.css +38 -2
- package/build/lexgui.js +383 -58
- package/build/lexgui.js.map +1 -1
- package/build/lexgui.min.css +1 -1
- package/build/lexgui.min.js +1 -1
- package/build/lexgui.module.js +383 -58
- package/build/lexgui.module.js.map +1 -1
- package/build/lexgui.module.min.js +1 -1
- package/changelog.md +28 -1
- package/examples/all-components.html +1 -0
- package/examples/asset-view.html +7 -33
- package/examples/code-editor.html +8 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
|
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
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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-
|
|
4463
|
+
iconClass = 'text-purple-500';
|
|
4303
4464
|
break;
|
|
4304
4465
|
case 'function':
|
|
4305
4466
|
iconName = 'Function';
|
|
4306
|
-
iconClass = 'text-
|
|
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.
|
|
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
|
|
4389
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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();
|