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.
- package/build/core/Namespace.js +1 -1
- package/build/core/Namespace.js.map +1 -1
- package/build/extensions/CodeEditor.d.ts +2 -1
- package/build/extensions/CodeEditor.js +233 -29
- package/build/extensions/CodeEditor.js.map +1 -1
- package/build/extensions/Timeline.js.map +1 -1
- package/build/extensions/VideoEditor.js +1021 -1022
- package/build/extensions/VideoEditor.js.map +1 -1
- package/build/lexgui.all.js +252 -33
- package/build/lexgui.all.js.map +1 -1
- package/build/lexgui.all.min.js +1 -1
- package/build/lexgui.all.module.js +252 -33
- package/build/lexgui.all.module.js.map +1 -1
- package/build/lexgui.all.module.min.js +1 -1
- package/build/lexgui.css +12 -2
- package/build/lexgui.js +251 -31
- 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 +251 -31
- package/build/lexgui.module.js.map +1 -1
- package/build/lexgui.module.min.js +1 -1
- package/changelog.md +13 -1
- package/demo.js +3 -2
- package/examples/code-editor.html +9 -0
- package/package.json +1 -1
package/build/core/Namespace.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Namespace.js","sources":["../../src/core/Namespace.ts"],"sourcesContent":["// Namespace.ts @jxarco\r\n\r\n/**\r\n * Main namespace\r\n * @namespace LX\r\n */\r\n\r\nconst g = globalThis as any;\r\n\r\n// Update global namespace if not present (Loading module)\r\n// Extension scripts rely on LX being globally available\r\nlet LX: any = g.LX;\r\n\r\nif ( !LX )\r\n{\r\n LX = {\r\n version: '8.3.
|
|
1
|
+
{"version":3,"file":"Namespace.js","sources":["../../src/core/Namespace.ts"],"sourcesContent":["// Namespace.ts @jxarco\r\n\r\n/**\r\n * Main namespace\r\n * @namespace LX\r\n */\r\n\r\nconst g = globalThis as any;\r\n\r\n// Update global namespace if not present (Loading module)\r\n// Extension scripts rely on LX being globally available\r\nlet LX: any = g.LX;\r\n\r\nif ( !LX )\r\n{\r\n LX = {\r\n version: '8.3.1',\r\n ready: false,\r\n extensions: [], // Store extensions used\r\n extraCommandbarEntries: [], // User specific entries for command bar\r\n signals: {}, // Events and triggers\r\n activeDraggable: null, // Watch for the current active draggable\r\n\r\n spacingMode: 'default',\r\n layoutMode: 'app',\r\n\r\n MOUSE_LEFT_CLICK: 0,\r\n MOUSE_MIDDLE_CLICK: 1,\r\n MOUSE_RIGHT_CLICK: 2,\r\n\r\n MOUSE_DOUBLE_CLICK: 2,\r\n MOUSE_TRIPLE_CLICK: 3,\r\n\r\n CURVE_MOVEOUT_CLAMP: 0,\r\n CURVE_MOVEOUT_DELETE: 1,\r\n\r\n DRAGGABLE_Z_INDEX: 101\r\n };\r\n\r\n g.LX = LX;\r\n}\r\n\r\nexport { LX };\r\n"],"names":[],"mappings":";AAAA;AAEA;;;AAGG;AAEH,MAAM,CAAC,GAAG,UAAiB;AAE3B;AACA;AACA,IAAI,EAAE,GAAQ,CAAC,CAAC;AAEhB,IAAK,CAAC,EAAE,EACR;AACI,IAAA,EAAE,GAAG;AACD,QAAA,OAAO,EAAE,OAAO;AAChB,QAAA,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,EAAE;QACd,sBAAsB,EAAE,EAAE;QAC1B,OAAO,EAAE,EAAE;QACX,eAAe,EAAE,IAAI;AAErB,QAAA,WAAW,EAAE,SAAS;AACtB,QAAA,UAAU,EAAE,KAAK;AAEjB,QAAA,gBAAgB,EAAE,CAAC;AACnB,QAAA,kBAAkB,EAAE,CAAC;AACrB,QAAA,iBAAiB,EAAE,CAAC;AAEpB,QAAA,kBAAkB,EAAE,CAAC;AACrB,QAAA,kBAAkB,EAAE,CAAC;AAErB,QAAA,mBAAmB,EAAE,CAAC;AACtB,QAAA,oBAAoB,EAAE,CAAC;AAEvB,QAAA,iBAAiB,EAAE;KACtB;AAED,IAAA,CAAC,CAAC,EAAE,GAAG,EAAE;AACb;;;;"}
|
|
@@ -349,7 +349,8 @@ export declare class CodeEditor {
|
|
|
349
349
|
private _init;
|
|
350
350
|
clear(): void;
|
|
351
351
|
addExplorerItem(item: any): void;
|
|
352
|
-
setText(text: string): void;
|
|
352
|
+
setText(text: string, language?: string, detectLang?: boolean): void;
|
|
353
|
+
private _detectLanguage;
|
|
353
354
|
appendText(text: string): void;
|
|
354
355
|
getText(): string;
|
|
355
356
|
setLanguage(name: string, extension?: string): void;
|
|
@@ -2137,6 +2137,21 @@ class CodeEditor {
|
|
|
2137
2137
|
this._inputArea.addEventListener('blur', () => this._setFocused(false));
|
|
2138
2138
|
this.codeArea.root.addEventListener('mousedown', this._onMouseDown.bind(this));
|
|
2139
2139
|
this.codeArea.root.addEventListener('contextmenu', this._onMouseDown.bind(this));
|
|
2140
|
+
this.codeArea.root.addEventListener('mouseover', (e) => {
|
|
2141
|
+
const link = e.target.closest('.code-link');
|
|
2142
|
+
if (link && e.ctrlKey)
|
|
2143
|
+
link.classList.add('hovered');
|
|
2144
|
+
});
|
|
2145
|
+
this.codeArea.root.addEventListener('mouseout', (e) => {
|
|
2146
|
+
const link = e.target.closest('.code-link');
|
|
2147
|
+
if (link)
|
|
2148
|
+
link.classList.remove('hovered');
|
|
2149
|
+
});
|
|
2150
|
+
this.codeArea.root.addEventListener('mousemove', (e) => {
|
|
2151
|
+
const link = e.target.closest('.code-link');
|
|
2152
|
+
if (link)
|
|
2153
|
+
link.classList.toggle('hovered', e.ctrlKey);
|
|
2154
|
+
});
|
|
2140
2155
|
// Bottom status panel
|
|
2141
2156
|
this.statusPanel = this._createStatusPanel(options);
|
|
2142
2157
|
if (this.statusPanel) {
|
|
@@ -2224,18 +2239,148 @@ class CodeEditor {
|
|
|
2224
2239
|
}
|
|
2225
2240
|
}
|
|
2226
2241
|
;
|
|
2227
|
-
setText(text) {
|
|
2242
|
+
setText(text, language, detectLang = false) {
|
|
2228
2243
|
if (!this.currentTab)
|
|
2229
2244
|
return;
|
|
2230
2245
|
this.doc.setText(text);
|
|
2231
2246
|
this.cursorSet.set(0, 0);
|
|
2232
2247
|
this.undoManager.clear();
|
|
2233
2248
|
this._lineStates = [];
|
|
2249
|
+
if (language) {
|
|
2250
|
+
this.setLanguage(language);
|
|
2251
|
+
}
|
|
2252
|
+
else if (detectLang) {
|
|
2253
|
+
const detected = this._detectLanguage(text);
|
|
2254
|
+
if (detected)
|
|
2255
|
+
this.setLanguage(detected);
|
|
2256
|
+
}
|
|
2234
2257
|
this._renderAllLines();
|
|
2235
2258
|
this._renderCursors();
|
|
2236
2259
|
this._renderSelections();
|
|
2237
2260
|
this.resize(true);
|
|
2238
2261
|
}
|
|
2262
|
+
_detectLanguage(text) {
|
|
2263
|
+
const scores = {};
|
|
2264
|
+
const add = (lang, pts) => { scores[lang] = (scores[lang] ?? 0) + pts; };
|
|
2265
|
+
// Score using reservedWords from each registered language
|
|
2266
|
+
const textWords = new Set(text.match(/\b\w+\b/g) ?? []);
|
|
2267
|
+
const totalWords = Math.max(textWords.size, 1);
|
|
2268
|
+
for (const langName of Tokenizer.getRegisteredLanguages()) {
|
|
2269
|
+
const langDef = Tokenizer.getLanguage(langName);
|
|
2270
|
+
if (!langDef?.reservedWords?.length)
|
|
2271
|
+
continue;
|
|
2272
|
+
let hits = 0;
|
|
2273
|
+
for (const word of langDef.reservedWords) {
|
|
2274
|
+
if (textWords.has(word))
|
|
2275
|
+
hits++;
|
|
2276
|
+
}
|
|
2277
|
+
if (hits > 0) {
|
|
2278
|
+
const vocabRatio = hits / langDef.reservedWords.length;
|
|
2279
|
+
const textRatio = hits / totalWords;
|
|
2280
|
+
add(langName, Math.round((vocabRatio + textRatio) * 40));
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
// Add scores based on "important" structural words only
|
|
2284
|
+
// HTML
|
|
2285
|
+
if (/<!DOCTYPE\s+html/i.test(text))
|
|
2286
|
+
add('HTML', 20);
|
|
2287
|
+
if (/<html[\s>]/i.test(text))
|
|
2288
|
+
add('HTML', 15);
|
|
2289
|
+
if (/<\/?(div|span|body|head|script|style|meta)\b/i.test(text))
|
|
2290
|
+
add('HTML', 8);
|
|
2291
|
+
// JSON — must come before JS checks (starts with { or [)
|
|
2292
|
+
if (/^\s*[\[{]/.test(text) && /"\s*:\s*/.test(text) && !/function|=>|const|var|let/.test(text))
|
|
2293
|
+
add('JSON', 15);
|
|
2294
|
+
// CSS
|
|
2295
|
+
if (/[\w-]+\s*:\s*[\w#\d"'(]+.*;/.test(text) && /[{}]/.test(text) && !/<\w+/.test(text))
|
|
2296
|
+
add('CSS', 10);
|
|
2297
|
+
if (/@(media|keyframes|import|charset|font-face)\b/.test(text))
|
|
2298
|
+
add('CSS', 15);
|
|
2299
|
+
// WGSL
|
|
2300
|
+
if (/@(vertex|fragment|compute|group|binding|builtin)\b/.test(text))
|
|
2301
|
+
add('WGSL', 20);
|
|
2302
|
+
if (/\bfn\s+\w+\s*\(/.test(text) && /\bvar\b/.test(text))
|
|
2303
|
+
add('WGSL', 10);
|
|
2304
|
+
if (/\b(vec2f|vec3f|vec4f|mat4x4f|f32|u32|i32)\b/.test(text))
|
|
2305
|
+
add('WGSL', 12);
|
|
2306
|
+
// GLSL
|
|
2307
|
+
if (/\b(gl_Position|gl_FragColor|gl_FragCoord)\b/.test(text))
|
|
2308
|
+
add('GLSL', 20);
|
|
2309
|
+
if (/\b(uniform|varying|attribute)\s+\w/.test(text))
|
|
2310
|
+
add('GLSL', 10);
|
|
2311
|
+
if (/\b(vec2|vec3|vec4|mat4|sampler2D)\b/.test(text) && !/vec2f|vec3f/.test(text))
|
|
2312
|
+
add('GLSL', 8);
|
|
2313
|
+
// HLSL
|
|
2314
|
+
if (/\b(SV_Position|SV_Target|SV_Depth)\b/.test(text))
|
|
2315
|
+
add('HLSL', 20);
|
|
2316
|
+
if (/\b(cbuffer|tbuffer|float4|float3|float2|Texture2D)\b/.test(text))
|
|
2317
|
+
add('HLSL', 12);
|
|
2318
|
+
// Python
|
|
2319
|
+
if (/^\s*def\s+\w+\s*\(/m.test(text))
|
|
2320
|
+
add('Python', 15);
|
|
2321
|
+
if (/^\s*import\s+\w/m.test(text) && !/\bfrom\s+['"]/.test(text))
|
|
2322
|
+
add('Python', 8);
|
|
2323
|
+
if (/^\s*class\s+\w+(\s*\(.*\))?:/m.test(text))
|
|
2324
|
+
add('Python', 10);
|
|
2325
|
+
if (/\bprint\s*\(/.test(text) && /:\s*$/.test(text))
|
|
2326
|
+
add('Python', 5);
|
|
2327
|
+
if (/elif\b|lambda\b|self\.\w/.test(text))
|
|
2328
|
+
add('Python', 8);
|
|
2329
|
+
// Rust
|
|
2330
|
+
if (/\bfn\s+\w+\s*\(/.test(text) && /\blet\s+mut\b/.test(text))
|
|
2331
|
+
add('Rust', 15);
|
|
2332
|
+
if (/\b(impl|trait|enum|struct)\s+\w+/.test(text) && /\bfn\b/.test(text))
|
|
2333
|
+
add('Rust', 12);
|
|
2334
|
+
if (/use\s+std::|use\s+\w+::\w+/.test(text))
|
|
2335
|
+
add('Rust', 10);
|
|
2336
|
+
if (/println!\s*\(/.test(text))
|
|
2337
|
+
add('Rust', 8);
|
|
2338
|
+
// C++
|
|
2339
|
+
if (/#include\s*<[\w./]+>/.test(text))
|
|
2340
|
+
add('C++', 15);
|
|
2341
|
+
if (/\bstd::\w+/.test(text))
|
|
2342
|
+
add('C++', 12);
|
|
2343
|
+
if (/\b(class|template|namespace|nullptr|new\s+\w)\b/.test(text))
|
|
2344
|
+
add('C++', 8);
|
|
2345
|
+
if (/(::|->)\w+/.test(text))
|
|
2346
|
+
add('C++', 6);
|
|
2347
|
+
// C
|
|
2348
|
+
if (/#include\s*<[\w.]+\.h>/.test(text))
|
|
2349
|
+
add('C', 12);
|
|
2350
|
+
if (/\bint\s+main\s*\(/.test(text))
|
|
2351
|
+
add('C', 15);
|
|
2352
|
+
if (/\b(printf|scanf|malloc|free|sizeof)\s*\(/.test(text))
|
|
2353
|
+
add('C', 10);
|
|
2354
|
+
// TypeScript — before JS (is a superset)
|
|
2355
|
+
if (/:\s*(string|number|boolean|void|any|never|unknown)\b/.test(text))
|
|
2356
|
+
add('TypeScript', 12);
|
|
2357
|
+
if (/\binterface\s+\w+/.test(text))
|
|
2358
|
+
add('TypeScript', 12);
|
|
2359
|
+
if (/\btype\s+\w+\s*=/.test(text))
|
|
2360
|
+
add('TypeScript', 10);
|
|
2361
|
+
if (/\bas\s+(string|number|any|unknown)\b/.test(text))
|
|
2362
|
+
add('TypeScript', 8);
|
|
2363
|
+
if (/\benum\s+\w+\s*\{/.test(text))
|
|
2364
|
+
add('TypeScript', 8);
|
|
2365
|
+
// JavaScript
|
|
2366
|
+
if (/\b(const|let|var)\s+\w+\s*=/.test(text))
|
|
2367
|
+
add('JavaScript', 8);
|
|
2368
|
+
if (/=>\s*[\w{(]/.test(text))
|
|
2369
|
+
add('JavaScript', 6);
|
|
2370
|
+
if (/\b(import|export)\s+(default|{|\*)/.test(text))
|
|
2371
|
+
add('JavaScript', 8);
|
|
2372
|
+
if (/\bconsole\.(log|warn|error)\b/.test(text))
|
|
2373
|
+
add('JavaScript', 6);
|
|
2374
|
+
// Markdown
|
|
2375
|
+
if (/^#{1,6}\s+\S/m.test(text))
|
|
2376
|
+
add('Markdown', 12);
|
|
2377
|
+
if (/\*\*\w.*?\*\*/.test(text) || /\[.+\]\(.+\)/.test(text))
|
|
2378
|
+
add('Markdown', 8);
|
|
2379
|
+
if (/^```\w*/m.test(text))
|
|
2380
|
+
add('Markdown', 10);
|
|
2381
|
+
const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
|
|
2382
|
+
return best && best[1] >= 8 ? best[0] : null;
|
|
2383
|
+
}
|
|
2239
2384
|
appendText(text) {
|
|
2240
2385
|
const cursor = this.cursorSet.getPrimary();
|
|
2241
2386
|
const { line, col } = cursor.head;
|
|
@@ -2734,8 +2879,8 @@ class CodeEditor {
|
|
|
2734
2879
|
: Tokenizer.initialState();
|
|
2735
2880
|
const lineText = this.doc.getLine(lineIndex);
|
|
2736
2881
|
const result = Tokenizer.tokenizeLine(lineText, this.language, prevState);
|
|
2737
|
-
// Build HTML
|
|
2738
2882
|
const langClass = this.language.name.toLowerCase().replace(/[^a-z]/g, '');
|
|
2883
|
+
const URL_REGEX = /(https?:\/\/[^\s"'<>)\]]+)/g;
|
|
2739
2884
|
let html = '';
|
|
2740
2885
|
for (const token of result.tokens) {
|
|
2741
2886
|
const cls = TOKEN_CLASS_MAP[token.type];
|
|
@@ -2743,11 +2888,15 @@ class CodeEditor {
|
|
|
2743
2888
|
.replace(/&/g, '&')
|
|
2744
2889
|
.replace(/</g, '<')
|
|
2745
2890
|
.replace(/>/g, '>');
|
|
2891
|
+
// Wrap URLs in comment tokens with a clickable span
|
|
2892
|
+
const content = (token.type === 'comment')
|
|
2893
|
+
? escaped.replace(URL_REGEX, `<span class="code-link" data-url="$1">$1</span>`)
|
|
2894
|
+
: escaped;
|
|
2746
2895
|
if (cls) {
|
|
2747
|
-
html += `<span class="${cls} ${langClass}">${
|
|
2896
|
+
html += `<span class="${cls} ${langClass}">${content}</span>`;
|
|
2748
2897
|
}
|
|
2749
2898
|
else {
|
|
2750
|
-
html +=
|
|
2899
|
+
html += content;
|
|
2751
2900
|
}
|
|
2752
2901
|
}
|
|
2753
2902
|
return { html: html || ' ', endState: result.state, tokens: result.tokens };
|
|
@@ -3577,11 +3726,25 @@ class CodeEditor {
|
|
|
3577
3726
|
const { line, col } = cursor.head;
|
|
3578
3727
|
const indent = this.doc.getIndent(line);
|
|
3579
3728
|
const spaces = ' '.repeat(indent);
|
|
3580
|
-
const
|
|
3581
|
-
this.
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3729
|
+
const charBefore = this.doc.getCharAt(line, col - 1);
|
|
3730
|
+
const charAfter = this.doc.getCharAt(line, col);
|
|
3731
|
+
const OPEN_CLOSE = { '{': '}', '[': ']', '(': ')' };
|
|
3732
|
+
if (charBefore && charAfter && OPEN_CLOSE[charBefore] === charAfter) {
|
|
3733
|
+
const innerSpaces = ' '.repeat(indent + this.tabSize);
|
|
3734
|
+
const insertion = '\n' + innerSpaces + '\n' + spaces;
|
|
3735
|
+
const op = this.doc.insert(line, col, insertion);
|
|
3736
|
+
this.undoManager.record(op, this.cursorSet.getCursorPositions());
|
|
3737
|
+
cursor.head = { line: line + 1, col: indent + this.tabSize };
|
|
3738
|
+
cursor.anchor = { ...cursor.head };
|
|
3739
|
+
this.cursorSet.adjustOthers(idx, line, col, 0, 2);
|
|
3740
|
+
}
|
|
3741
|
+
else {
|
|
3742
|
+
const op = this.doc.insert(line, col, '\n' + spaces);
|
|
3743
|
+
this.undoManager.record(op, this.cursorSet.getCursorPositions());
|
|
3744
|
+
cursor.head = { line: line + 1, col: indent };
|
|
3745
|
+
cursor.anchor = { ...cursor.head };
|
|
3746
|
+
this.cursorSet.adjustOthers(idx, line, col, 0, 1);
|
|
3747
|
+
}
|
|
3585
3748
|
}
|
|
3586
3749
|
this._rebuildLines();
|
|
3587
3750
|
this._afterCursorMove();
|
|
@@ -3595,8 +3758,36 @@ class CodeEditor {
|
|
|
3595
3758
|
for (const idx of this.cursorSet.sortedIndicesBottomUp()) {
|
|
3596
3759
|
const cursor = this.cursorSet.cursors[idx];
|
|
3597
3760
|
const { line, col } = cursor.head;
|
|
3598
|
-
|
|
3599
|
-
|
|
3761
|
+
const anchorLine = cursor.anchor.line;
|
|
3762
|
+
// Multiline selection: indent/dedent all lines in the selection
|
|
3763
|
+
const startLine = Math.min(line, anchorLine);
|
|
3764
|
+
const endLine = Math.max(line, anchorLine);
|
|
3765
|
+
const isMultiline = startLine !== endLine;
|
|
3766
|
+
if (isMultiline) {
|
|
3767
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
3768
|
+
if (shift) {
|
|
3769
|
+
const lineText = this.doc.getLine(i);
|
|
3770
|
+
let spacesToRemove = 0;
|
|
3771
|
+
while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
|
|
3772
|
+
spacesToRemove++;
|
|
3773
|
+
}
|
|
3774
|
+
if (spacesToRemove > 0) {
|
|
3775
|
+
const op = this.doc.delete(i, 0, spacesToRemove);
|
|
3776
|
+
this.undoManager.record(op, this.cursorSet.getCursorPositions());
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
else {
|
|
3780
|
+
const spaces = ' '.repeat(this.tabSize);
|
|
3781
|
+
const op = this.doc.insert(i, 0, spaces);
|
|
3782
|
+
this.undoManager.record(op, this.cursorSet.getCursorPositions());
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
const delta = shift ? -this.tabSize : this.tabSize;
|
|
3786
|
+
cursor.head = { line, col: Math.max(0, col + delta) };
|
|
3787
|
+
cursor.anchor = { line: anchorLine, col: Math.max(0, cursor.anchor.col + delta) };
|
|
3788
|
+
}
|
|
3789
|
+
else if (shift) {
|
|
3790
|
+
// Single line dedent: remove up to tabSize spaces from start
|
|
3600
3791
|
const lineText = this.doc.getLine(line);
|
|
3601
3792
|
let spacesToRemove = 0;
|
|
3602
3793
|
while (spacesToRemove < this.tabSize && spacesToRemove < lineText.length && lineText[spacesToRemove] === ' ') {
|
|
@@ -3611,6 +3802,7 @@ class CodeEditor {
|
|
|
3611
3802
|
}
|
|
3612
3803
|
}
|
|
3613
3804
|
else {
|
|
3805
|
+
// Single line indent: insert spaces at cursor
|
|
3614
3806
|
const spacesToAdd = this.tabSize - (col % this.tabSize);
|
|
3615
3807
|
const spaces = ' '.repeat(spacesToAdd);
|
|
3616
3808
|
const op = this.doc.insert(line, col, spaces);
|
|
@@ -3644,8 +3836,10 @@ class CodeEditor {
|
|
|
3644
3836
|
this.cursorSet.adjustOthers(idx, start.line, start.col, -colDelta, -linesRemoved);
|
|
3645
3837
|
anyDeleted = true;
|
|
3646
3838
|
}
|
|
3647
|
-
if (anyDeleted)
|
|
3839
|
+
if (anyDeleted) {
|
|
3648
3840
|
this._rebuildLines();
|
|
3841
|
+
this._doHideAutocomplete();
|
|
3842
|
+
}
|
|
3649
3843
|
}
|
|
3650
3844
|
// Clipboard helpers:
|
|
3651
3845
|
_doCopy() {
|
|
@@ -3759,6 +3953,15 @@ class CodeEditor {
|
|
|
3759
3953
|
return;
|
|
3760
3954
|
if (this.autocomplete && this.autocomplete.contains(e.target))
|
|
3761
3955
|
return;
|
|
3956
|
+
// Ctrl+click: open link if cursor is over a code-link span
|
|
3957
|
+
if (e.ctrlKey && e.button === 0) {
|
|
3958
|
+
const target = e.target;
|
|
3959
|
+
const link = target.closest('.code-link');
|
|
3960
|
+
if (link?.dataset.url) {
|
|
3961
|
+
window.open(link.dataset.url, '_blank');
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3762
3965
|
e.preventDefault(); // Prevent browser from stealing focus from _inputArea
|
|
3763
3966
|
this._wasPaired = false;
|
|
3764
3967
|
// Calculate line and column from click position
|
|
@@ -3876,9 +4079,9 @@ class CodeEditor {
|
|
|
3876
4079
|
}
|
|
3877
4080
|
const suggestions = [];
|
|
3878
4081
|
const added = new Set();
|
|
3879
|
-
const addSuggestion = (label, kind, scope, detail) => {
|
|
4082
|
+
const addSuggestion = (label, kind, scope, detail, insertText) => {
|
|
3880
4083
|
if (!added.has(label)) {
|
|
3881
|
-
suggestions.push({ label, kind, scope, detail });
|
|
4084
|
+
suggestions.push({ label, kind, scope, detail, insertText });
|
|
3882
4085
|
added.add(label);
|
|
3883
4086
|
}
|
|
3884
4087
|
};
|
|
@@ -3900,8 +4103,9 @@ class CodeEditor {
|
|
|
3900
4103
|
const label = typeof suggestion === 'string' ? suggestion : suggestion.label;
|
|
3901
4104
|
const kind = typeof suggestion === 'object' ? suggestion.kind : undefined;
|
|
3902
4105
|
const detail = typeof suggestion === 'object' ? suggestion.detail : undefined;
|
|
4106
|
+
const insertText = typeof suggestion === 'object' ? suggestion.insertText : suggestion;
|
|
3903
4107
|
if (label.toLowerCase().startsWith(word.toLowerCase())) {
|
|
3904
|
-
addSuggestion(label, kind, undefined, detail);
|
|
4108
|
+
addSuggestion(label, kind, undefined, detail, insertText);
|
|
3905
4109
|
}
|
|
3906
4110
|
}
|
|
3907
4111
|
// Close autocomplete if no suggestions
|
|
@@ -3921,6 +4125,7 @@ class CodeEditor {
|
|
|
3921
4125
|
// Render suggestions
|
|
3922
4126
|
suggestions.forEach((suggestion, index) => {
|
|
3923
4127
|
const item = document.createElement('pre');
|
|
4128
|
+
item.insertText = suggestion.insertText ?? suggestion.label;
|
|
3924
4129
|
if (index === this._selectedAutocompleteIndex)
|
|
3925
4130
|
item.classList.add('selected');
|
|
3926
4131
|
const currSuggestion = suggestion.label;
|
|
@@ -4039,8 +4244,8 @@ class CodeEditor {
|
|
|
4039
4244
|
* Insert the selected autocomplete word at cursor.
|
|
4040
4245
|
*/
|
|
4041
4246
|
_doAutocompleteWord() {
|
|
4042
|
-
const
|
|
4043
|
-
if (!
|
|
4247
|
+
const text = this._getSelectedAutoCompleteWord();
|
|
4248
|
+
if (!text)
|
|
4044
4249
|
return;
|
|
4045
4250
|
const cursor = this.cursorSet.getPrimary().head;
|
|
4046
4251
|
const { start, end } = this._getWordAtCursor();
|
|
@@ -4050,8 +4255,14 @@ class CodeEditor {
|
|
|
4050
4255
|
const deleteOp = this.doc.delete(line, start, end - start);
|
|
4051
4256
|
this.undoManager.record(deleteOp, cursorsBefore);
|
|
4052
4257
|
}
|
|
4053
|
-
const insertOp = this.doc.insert(line, start,
|
|
4054
|
-
|
|
4258
|
+
const insertOp = this.doc.insert(line, start, text);
|
|
4259
|
+
const insertedLines = text.split(/\r?\n/);
|
|
4260
|
+
if (insertedLines.length === 1) {
|
|
4261
|
+
this.cursorSet.set(line, start + text.length);
|
|
4262
|
+
}
|
|
4263
|
+
else {
|
|
4264
|
+
this.cursorSet.set(line + insertedLines.length - 1, insertedLines[insertedLines.length - 1].length);
|
|
4265
|
+
}
|
|
4055
4266
|
const cursorsAfter = this.cursorSet.getCursorPositions();
|
|
4056
4267
|
this.undoManager.record(insertOp, cursorsAfter);
|
|
4057
4268
|
this._rebuildLines();
|
|
@@ -4062,15 +4273,7 @@ class CodeEditor {
|
|
|
4062
4273
|
if (!this.autocomplete || !this._isAutoCompleteActive)
|
|
4063
4274
|
return null;
|
|
4064
4275
|
const pre = this.autocomplete.childNodes[this._selectedAutocompleteIndex];
|
|
4065
|
-
|
|
4066
|
-
for (let childSpan of pre.childNodes) {
|
|
4067
|
-
const span = childSpan;
|
|
4068
|
-
if (span.constructor != HTMLSpanElement || span.classList.contains('kind')) {
|
|
4069
|
-
continue;
|
|
4070
|
-
}
|
|
4071
|
-
word += span.textContent;
|
|
4072
|
-
}
|
|
4073
|
-
return word;
|
|
4276
|
+
return pre.insertText;
|
|
4074
4277
|
}
|
|
4075
4278
|
_afterCursorMove() {
|
|
4076
4279
|
this._renderCursors();
|
|
@@ -4103,7 +4306,8 @@ class CodeEditor {
|
|
|
4103
4306
|
_resetGutter() {
|
|
4104
4307
|
// Use cached value or compute if not available (e.g., on initial load)
|
|
4105
4308
|
const tabsHeight = this._cachedTabsHeight || (this.tabs?.root.getBoundingClientRect().height ?? 0);
|
|
4106
|
-
this.
|
|
4309
|
+
const statusPanelHeight = this._cachedStatusPanelHeight || (this.statusPanel?.root.getBoundingClientRect().height ?? 0);
|
|
4310
|
+
this.lineGutter.style.height = `calc(100% - ${tabsHeight + statusPanelHeight}px)`;
|
|
4107
4311
|
}
|
|
4108
4312
|
getMaxLineLength() {
|
|
4109
4313
|
if (!this.currentTab)
|