mrmd-editor 0.7.1 → 0.8.0
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/package.json +3 -1
- package/src/commands.js +112 -4
- package/src/comment-syntax.js +364 -39
- package/src/config/handlers.js +1 -2
- package/src/config/schema.js +46 -4
- package/src/document-template.js +2236 -0
- package/src/frontmatter-updater.js +204 -74
- package/src/grammar.js +758 -0
- package/src/index.js +1074 -55
- package/src/keymap.js +11 -2
- package/src/markdown/block-decorations.js +108 -5
- package/src/markdown/facets.js +37 -0
- package/src/markdown/html-inline.js +9 -5
- package/src/markdown/index.js +13 -3
- package/src/markdown/inline-commands.js +256 -0
- package/src/markdown/inline-model.js +578 -0
- package/src/markdown/inline-state.js +103 -0
- package/src/markdown/renderer.js +219 -12
- package/src/markdown/styles.js +290 -3
- package/src/markdown/widgets/alert-title.js +10 -8
- package/src/markdown/widgets/frontmatter.js +0 -6
- package/src/markdown/widgets/index.js +1 -0
- package/src/markdown/widgets/list-marker.js +29 -0
- package/src/markdown/wysiwyg.js +1158 -0
- package/src/mrp-types.js +2 -0
- package/src/output-widget.js +532 -18
- package/src/page-view-pagination.js +127 -0
- package/src/runtime-lsp.js +1757 -150
- package/src/section-controls/commands.js +617 -0
- package/src/section-controls/index.js +63 -0
- package/src/section-controls/plugin.js +165 -0
- package/src/section-controls/widgets.js +936 -0
- package/src/shell/ai-menu.js +11 -0
- package/src/shell/components/context-panel.js +572 -0
- package/src/shell/components/status-bar.js +10 -2
- package/src/shell/layouts/studio.js +206 -14
- package/src/shell/orchestrator-client.js +69 -0
- package/src/spellcheck.js +166 -0
- package/src/tables/README.md +97 -0
- package/src/tables/commands/insert-linked-table.js +122 -0
- package/src/tables/commands/open-table-workspace.js +43 -0
- package/src/tables/index.js +24 -0
- package/src/tables/jobs/client.js +158 -0
- package/src/tables/parsing/anchors.js +82 -0
- package/src/tables/parsing/linked-table-blocks.js +61 -0
- package/src/tables/state/linked-table-state.js +68 -0
- package/src/tables/widgets/linked-table-source-banner.js +77 -0
- package/src/tables/widgets/linked-table-widget.js +256 -0
- package/src/tables/workspace/controller.js +616 -0
- package/src/term-pty-client.js +51 -2
- package/src/term-widget.js +43 -3
- package/src/widgets/theme-utils.js +24 -16
- package/src/widgets/theme.js +1015 -1
- package/src/runtime-codelens/detector.js +0 -279
- package/src/runtime-codelens/index.js +0 -76
- package/src/runtime-codelens/plugin.js +0 -142
- package/src/runtime-codelens/styles.js +0 -184
- package/src/runtime-codelens/widgets.js +0 -216
package/src/runtime-lsp.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* @module runtime-lsp
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { hoverTooltip } from '@codemirror/view';
|
|
20
|
-
import {
|
|
19
|
+
import { hoverTooltip, closeHoverTooltips, showTooltip, ViewPlugin } from '@codemirror/view';
|
|
20
|
+
import { StateField, StateEffect } from '@codemirror/state';
|
|
21
|
+
import { autocompletion, startCompletion } from '@codemirror/autocomplete';
|
|
21
22
|
import { getCellAtCursor, findCells } from './cells.js';
|
|
22
23
|
|
|
23
24
|
// #region INTERFACES
|
|
@@ -42,6 +43,7 @@ import { getCellAtCursor, findCells } from './cells.js';
|
|
|
42
43
|
* @property {string} [value]
|
|
43
44
|
* @property {string} [signature]
|
|
44
45
|
* @property {string} [documentation]
|
|
46
|
+
* @property {string} [docstring]
|
|
45
47
|
*/
|
|
46
48
|
|
|
47
49
|
/**
|
|
@@ -61,6 +63,9 @@ import { getCellAtCursor, findCells } from './cells.js';
|
|
|
61
63
|
* @property {string} [valuePreview]
|
|
62
64
|
* @property {string} [documentation]
|
|
63
65
|
* @property {string} [insertText]
|
|
66
|
+
* @property {number} [boost]
|
|
67
|
+
* @property {string} [sortText]
|
|
68
|
+
* @property {string|{name: string, rank?: number|'dynamic'}} [section]
|
|
64
69
|
*/
|
|
65
70
|
|
|
66
71
|
/**
|
|
@@ -132,6 +137,8 @@ export function adaptMrmdJsSession(session) {
|
|
|
132
137
|
type: result.type,
|
|
133
138
|
value: result.value,
|
|
134
139
|
signature: result.signature,
|
|
140
|
+
documentation: result.documentation || result.docstring,
|
|
141
|
+
docstring: result.docstring,
|
|
135
142
|
};
|
|
136
143
|
} catch (e) {
|
|
137
144
|
console.warn('mrmd-js hover error:', e);
|
|
@@ -229,7 +236,10 @@ export function adaptMRPClient(client, languages) {
|
|
|
229
236
|
try {
|
|
230
237
|
const result = await client.hover({ code, cursor });
|
|
231
238
|
if (!result || !result.found) return null;
|
|
232
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
...result,
|
|
241
|
+
documentation: result.documentation || result.docstring,
|
|
242
|
+
};
|
|
233
243
|
} catch (e) {
|
|
234
244
|
console.warn('MRP hover error:', e);
|
|
235
245
|
return null;
|
|
@@ -316,151 +326,1346 @@ function getLanguageAtPosition(content, pos) {
|
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
/**
|
|
319
|
-
* Get code within a cell for a given position.
|
|
320
|
-
* Returns the code and the offset of the cursor within that code.
|
|
329
|
+
* Get code within a cell for a given position.
|
|
330
|
+
* Returns the code and the offset of the cursor within that code.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} content - Document content
|
|
333
|
+
* @param {number} pos - Document position
|
|
334
|
+
* @returns {{code: string, offset: number, language: string, cell: Object}|null}
|
|
335
|
+
*/
|
|
336
|
+
function getCodeAtPosition(content, pos) {
|
|
337
|
+
const cell = getCellAtCursor(content, pos);
|
|
338
|
+
if (!cell) return null;
|
|
339
|
+
|
|
340
|
+
// Calculate offset within the cell's code
|
|
341
|
+
const offset = pos - cell.codeStart;
|
|
342
|
+
if (offset < 0 || offset > cell.code.length) return null;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
code: cell.code,
|
|
346
|
+
offset,
|
|
347
|
+
language: cell.language,
|
|
348
|
+
cell,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// #endregion LANGUAGE_DETECTION
|
|
353
|
+
|
|
354
|
+
// #region HOVER_EXTENSION
|
|
355
|
+
|
|
356
|
+
const setPinnedHoverTooltip = StateEffect.define();
|
|
357
|
+
const clearPinnedHoverTooltip = StateEffect.define();
|
|
358
|
+
|
|
359
|
+
const pinnedHoverTooltipField = StateField.define({
|
|
360
|
+
create() {
|
|
361
|
+
return null;
|
|
362
|
+
},
|
|
363
|
+
update(value, tr) {
|
|
364
|
+
if (tr.docChanged) return null;
|
|
365
|
+
|
|
366
|
+
for (const effect of tr.effects) {
|
|
367
|
+
if (effect.is(setPinnedHoverTooltip)) return effect.value;
|
|
368
|
+
if (effect.is(clearPinnedHoverTooltip)) return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return value;
|
|
372
|
+
},
|
|
373
|
+
provide: (f) => showTooltip.from(f),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
function looksLikeFunctionRepr(value) {
|
|
377
|
+
return typeof value === 'string' && /^<function\s+[^>]+\s+at\s+0x[0-9a-f]+>$/i.test(value.trim());
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function cleanDocsText(text) {
|
|
381
|
+
if (!text) return '';
|
|
382
|
+
return String(text)
|
|
383
|
+
.replace(/(?:<function\s+[^>]+\s+at\s+0x[0-9a-f]+>)+\s*$/ig, '')
|
|
384
|
+
.trim();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function formatHoverText(result) {
|
|
388
|
+
if (!result) return '';
|
|
389
|
+
const parts = [];
|
|
390
|
+
|
|
391
|
+
const nameType = [result.name, result.type].filter(Boolean).join(' : ');
|
|
392
|
+
if (nameType) parts.push(nameType);
|
|
393
|
+
if (result.signature) parts.push(result.signature);
|
|
394
|
+
|
|
395
|
+
const suppressValue = !!result.signature && looksLikeFunctionRepr(result.value);
|
|
396
|
+
if (result.value && !suppressValue) parts.push(result.value);
|
|
397
|
+
|
|
398
|
+
const docsText = cleanDocsText(result.documentation || result.docstring);
|
|
399
|
+
if (docsText) parts.push(docsText);
|
|
400
|
+
|
|
401
|
+
if (result.file) {
|
|
402
|
+
parts.push(`Source: ${result.file}${result.line ? `:${result.line}` : ''}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return parts.join('\n\n');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createHoverTooltipDescriptor(view, hoverResult, pos, end, { sticky = false } = {}) {
|
|
409
|
+
return {
|
|
410
|
+
pos,
|
|
411
|
+
end,
|
|
412
|
+
above: false,
|
|
413
|
+
arrow: true,
|
|
414
|
+
create() {
|
|
415
|
+
const dom = document.createElement('div');
|
|
416
|
+
dom.className = `mrmd-runtime-hover${sticky ? ' mrmd-runtime-hover-sticky' : ''}`;
|
|
417
|
+
dom.innerHTML = formatHoverContent(hoverResult);
|
|
418
|
+
|
|
419
|
+
const copyBtn = dom.querySelector('.mrmd-hover-copy');
|
|
420
|
+
if (copyBtn) {
|
|
421
|
+
copyBtn.addEventListener('mousedown', (event) => {
|
|
422
|
+
event.stopPropagation();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
copyBtn.addEventListener('click', async (event) => {
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
event.stopPropagation();
|
|
428
|
+
|
|
429
|
+
const text = formatHoverText(hoverResult);
|
|
430
|
+
if (!text) return;
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
await navigator.clipboard?.writeText(text);
|
|
434
|
+
const original = copyBtn.textContent;
|
|
435
|
+
copyBtn.textContent = 'Copied';
|
|
436
|
+
setTimeout(() => {
|
|
437
|
+
copyBtn.textContent = original || 'Copy';
|
|
438
|
+
}, 900);
|
|
439
|
+
} catch {
|
|
440
|
+
// ignore
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const sourceLink = dom.querySelector('.mrmd-hover-source-link');
|
|
446
|
+
if (sourceLink) {
|
|
447
|
+
sourceLink.addEventListener('mousedown', (event) => {
|
|
448
|
+
event.stopPropagation();
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!sticky) {
|
|
453
|
+
dom.addEventListener('mousedown', (event) => {
|
|
454
|
+
if (event.button !== 0) return;
|
|
455
|
+
if (event.target instanceof Element && event.target.closest('.mrmd-hover-copy')) return;
|
|
456
|
+
|
|
457
|
+
const stickyTooltip = createHoverTooltipDescriptor(view, hoverResult, pos, end, { sticky: true });
|
|
458
|
+
view.dispatch({
|
|
459
|
+
effects: [
|
|
460
|
+
setPinnedHoverTooltip.of(stickyTooltip),
|
|
461
|
+
closeHoverTooltips,
|
|
462
|
+
],
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
dom,
|
|
469
|
+
offset: { x: 0, y: -8 },
|
|
470
|
+
overlap: true,
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const pinnedHoverClosePlugin = ViewPlugin.fromClass(
|
|
477
|
+
class {
|
|
478
|
+
constructor(view) {
|
|
479
|
+
this.view = view;
|
|
480
|
+
this.onMouseDownCapture = (event) => {
|
|
481
|
+
const pinned = view.state.field(pinnedHoverTooltipField, false);
|
|
482
|
+
if (!pinned) return;
|
|
483
|
+
|
|
484
|
+
const target = event.target;
|
|
485
|
+
if (target instanceof Element && target.closest('.mrmd-runtime-hover')) return;
|
|
486
|
+
|
|
487
|
+
view.dispatch({ effects: clearPinnedHoverTooltip.of(null) });
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
view.dom.ownerDocument.addEventListener('mousedown', this.onMouseDownCapture, true);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
destroy() {
|
|
494
|
+
this.view.dom.ownerDocument.removeEventListener('mousedown', this.onMouseDownCapture, true);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Create a CodeMirror hover tooltip extension powered by runtime LSP.
|
|
501
|
+
*
|
|
502
|
+
* Shows actual runtime values when hovering over variables/symbols.
|
|
503
|
+
*
|
|
504
|
+
* @param {Object} options
|
|
505
|
+
* @param {Map<string, RuntimeLSPProvider>} options.providers - Language → provider map
|
|
506
|
+
* @param {function(): string} options.getContent - Get document content
|
|
507
|
+
* @param {import('./awareness/state.js').AwarenessStateManager} [options.stateManager] - For awareness broadcast
|
|
508
|
+
* @param {import('yjs').Text} [options.yText] - For RelativePosition tracking
|
|
509
|
+
* @returns {import('@codemirror/state').Extension}
|
|
510
|
+
*/
|
|
511
|
+
export function createRuntimeHoverExtension({ providers, getContent, stateManager, yText }) {
|
|
512
|
+
const hoverExtension = hoverTooltip(
|
|
513
|
+
async (view, pos, side) => {
|
|
514
|
+
const content = getContent();
|
|
515
|
+
const codeInfo = getCodeAtPosition(content, pos);
|
|
516
|
+
|
|
517
|
+
if (!codeInfo) return null;
|
|
518
|
+
|
|
519
|
+
// Find provider for this language
|
|
520
|
+
const provider = findProviderForLanguage(providers, codeInfo.language);
|
|
521
|
+
if (!provider) return null;
|
|
522
|
+
|
|
523
|
+
// Get hover info from runtime
|
|
524
|
+
let hoverResult = await provider.hover(codeInfo.code, codeInfo.offset, codeInfo.language);
|
|
525
|
+
if (!hoverResult || !hoverResult.found) return null;
|
|
526
|
+
|
|
527
|
+
// If hover payload is minimal, enrich with inspect docstring/signature/source metadata.
|
|
528
|
+
// This keeps hover snappy for runtimes that already return rich hover data,
|
|
529
|
+
// while still showing docs/location for runtimes that only return basic hover fields.
|
|
530
|
+
const needsInspectEnrichment =
|
|
531
|
+
(!hoverResult.documentation && !hoverResult.docstring) ||
|
|
532
|
+
(!hoverResult.file && !hoverResult.line);
|
|
533
|
+
|
|
534
|
+
if (needsInspectEnrichment && provider.inspect) {
|
|
535
|
+
try {
|
|
536
|
+
const inspectResult = await provider.inspect(
|
|
537
|
+
codeInfo.code,
|
|
538
|
+
codeInfo.offset,
|
|
539
|
+
codeInfo.language,
|
|
540
|
+
{ detail: 1 }
|
|
541
|
+
);
|
|
542
|
+
if (inspectResult?.found) {
|
|
543
|
+
hoverResult = {
|
|
544
|
+
...inspectResult,
|
|
545
|
+
...hoverResult,
|
|
546
|
+
// Prefer explicit hover value/name if present, but fill missing docs/signature/location.
|
|
547
|
+
documentation:
|
|
548
|
+
hoverResult.documentation ||
|
|
549
|
+
hoverResult.docstring ||
|
|
550
|
+
inspectResult.documentation ||
|
|
551
|
+
inspectResult.docstring,
|
|
552
|
+
docstring: hoverResult.docstring || inspectResult.docstring,
|
|
553
|
+
signature: hoverResult.signature || inspectResult.signature,
|
|
554
|
+
file: hoverResult.file || inspectResult.file,
|
|
555
|
+
line: hoverResult.line || inspectResult.line,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
} catch {
|
|
559
|
+
// Ignore enrichment errors; base hover still works.
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Broadcast to awareness if available
|
|
564
|
+
if (stateManager) {
|
|
565
|
+
const position = yText
|
|
566
|
+
? await import('yjs').then(Y => Y.createRelativePositionFromTypeIndex(yText, pos))
|
|
567
|
+
: { line: view.state.doc.lineAt(pos).number, ch: pos - view.state.doc.lineAt(pos).from };
|
|
568
|
+
|
|
569
|
+
stateManager.setHover({
|
|
570
|
+
symbol: hoverResult.name,
|
|
571
|
+
type: hoverResult.type,
|
|
572
|
+
info: hoverResult.value || hoverResult.signature,
|
|
573
|
+
position,
|
|
574
|
+
cellIndex: getCellIndex(content, codeInfo.cell),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Create tooltip DOM
|
|
579
|
+
return createHoverTooltipDescriptor(
|
|
580
|
+
view,
|
|
581
|
+
hoverResult,
|
|
582
|
+
pos,
|
|
583
|
+
pos + Math.max(1, hoverResult.name?.length || 0),
|
|
584
|
+
{ sticky: false }
|
|
585
|
+
);
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
hoverTime: 300,
|
|
589
|
+
hideOnChange: false,
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
return [
|
|
594
|
+
hoverExtension,
|
|
595
|
+
pinnedHoverTooltipField,
|
|
596
|
+
pinnedHoverClosePlugin,
|
|
597
|
+
];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Format hover content as HTML.
|
|
602
|
+
*
|
|
603
|
+
* @param {HoverResult} result
|
|
604
|
+
* @returns {string}
|
|
605
|
+
*/
|
|
606
|
+
function formatHoverContent(result) {
|
|
607
|
+
let html = '<div class="mrmd-hover-content">';
|
|
608
|
+
|
|
609
|
+
// Header row
|
|
610
|
+
html += '<div class="mrmd-hover-header">';
|
|
611
|
+
html += '<div class="mrmd-hover-name">';
|
|
612
|
+
|
|
613
|
+
if (result.name) {
|
|
614
|
+
html += `<code>${escapeHtml(result.name)}</code>`;
|
|
615
|
+
}
|
|
616
|
+
if (result.type) {
|
|
617
|
+
html += ` <span class="mrmd-hover-type">${escapeHtml(result.type)}</span>`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
html += '</div>';
|
|
621
|
+
html += '<button class="mrmd-hover-copy" type="button" title="Copy hover details">Copy</button>';
|
|
622
|
+
html += '</div>';
|
|
623
|
+
|
|
624
|
+
// Signature (for functions)
|
|
625
|
+
if (result.signature) {
|
|
626
|
+
html += `<div class="mrmd-hover-signature"><code>${escapeHtml(result.signature)}</code></div>`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Value preview (skip noisy function repr when we already have signature)
|
|
630
|
+
const suppressValue = !!result.signature && looksLikeFunctionRepr(result.value);
|
|
631
|
+
if (result.value && !suppressValue) {
|
|
632
|
+
html += `<div class="mrmd-hover-value">${escapeHtml(result.value)}</div>`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Documentation / docstring
|
|
636
|
+
const docsText = cleanDocsText(result.documentation || result.docstring);
|
|
637
|
+
if (docsText) {
|
|
638
|
+
html += `<div class="mrmd-hover-docs">${escapeHtml(docsText)}</div>`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Source location (when provided by runtime inspect)
|
|
642
|
+
if (result.file) {
|
|
643
|
+
const locationText = `${result.file}${result.line ? `:${result.line}` : ''}`;
|
|
644
|
+
if (result.file.startsWith('/')) {
|
|
645
|
+
const fileHref = `file://${encodeURI(result.file)}`;
|
|
646
|
+
html += `<div class="mrmd-hover-source">Source: <a class="mrmd-hover-source-link" href="${escapeAttr(fileHref)}" target="_blank" rel="noopener noreferrer">${escapeHtml(locationText)}</a></div>`;
|
|
647
|
+
} else {
|
|
648
|
+
html += `<div class="mrmd-hover-source">Source: ${escapeHtml(locationText)}</div>`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
html += '</div>';
|
|
653
|
+
return html;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Escape HTML special characters.
|
|
658
|
+
*
|
|
659
|
+
* @param {string} str
|
|
660
|
+
* @returns {string}
|
|
661
|
+
*/
|
|
662
|
+
function escapeHtml(str) {
|
|
663
|
+
if (!str) return '';
|
|
664
|
+
return str
|
|
665
|
+
.replace(/&/g, '&')
|
|
666
|
+
.replace(/</g, '<')
|
|
667
|
+
.replace(/>/g, '>')
|
|
668
|
+
.replace(/"/g, '"');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function escapeAttr(str) {
|
|
672
|
+
if (!str) return '';
|
|
673
|
+
return String(str)
|
|
674
|
+
.replace(/&/g, '&')
|
|
675
|
+
.replace(/"/g, '"')
|
|
676
|
+
.replace(/'/g, ''')
|
|
677
|
+
.replace(/</g, '<')
|
|
678
|
+
.replace(/>/g, '>');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// #endregion HOVER_EXTENSION
|
|
682
|
+
|
|
683
|
+
// #region COMPLETION_EXTENSION
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Check whether the character at a given index is escaped.
|
|
687
|
+
*
|
|
688
|
+
* @param {string} text
|
|
689
|
+
* @param {number} index
|
|
690
|
+
* @returns {boolean}
|
|
691
|
+
*/
|
|
692
|
+
function isEscapedAt(text, index) {
|
|
693
|
+
let backslashes = 0;
|
|
694
|
+
for (let i = index - 1; i >= 0 && text[i] === '\\'; i--) {
|
|
695
|
+
backslashes++;
|
|
696
|
+
}
|
|
697
|
+
return backslashes % 2 === 1;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Split a source range on a delimiter, ignoring nested (), [], {}, strings, and comments.
|
|
702
|
+
*
|
|
703
|
+
* @param {string} code
|
|
704
|
+
* @param {number} start
|
|
705
|
+
* @param {number} end
|
|
706
|
+
* @param {string} [delimiter=',']
|
|
707
|
+
* @returns {Array<{start: number, end: number, text: string}>}
|
|
708
|
+
*/
|
|
709
|
+
function splitTopLevelRange(code, start, end, delimiter = ',') {
|
|
710
|
+
/** @type {Array<{start: number, end: number, text: string}>} */
|
|
711
|
+
const segments = [];
|
|
712
|
+
|
|
713
|
+
let segmentStart = start;
|
|
714
|
+
const stack = [];
|
|
715
|
+
let stringQuote = null;
|
|
716
|
+
let lineComment = false;
|
|
717
|
+
let blockComment = false;
|
|
718
|
+
|
|
719
|
+
for (let i = start; i < end; i++) {
|
|
720
|
+
const ch = code[i];
|
|
721
|
+
const next = i + 1 < end ? code[i + 1] : '';
|
|
722
|
+
|
|
723
|
+
if (lineComment) {
|
|
724
|
+
if (ch === '\n') lineComment = false;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (blockComment) {
|
|
729
|
+
if (ch === '*' && next === '/') {
|
|
730
|
+
blockComment = false;
|
|
731
|
+
i++;
|
|
732
|
+
}
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (stringQuote) {
|
|
737
|
+
if (ch === stringQuote && !isEscapedAt(code, i)) {
|
|
738
|
+
stringQuote = null;
|
|
739
|
+
}
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (ch === '#' || (ch === '/' && next === '/')) {
|
|
744
|
+
lineComment = true;
|
|
745
|
+
if (ch === '/') i++;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (ch === '/' && next === '*') {
|
|
750
|
+
blockComment = true;
|
|
751
|
+
i++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
756
|
+
stringQuote = ch;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
761
|
+
stack.push(ch);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
766
|
+
const expected = ch === ')' ? '(' : ch === ']' ? '[' : '{';
|
|
767
|
+
if (stack[stack.length - 1] === expected) {
|
|
768
|
+
stack.pop();
|
|
769
|
+
}
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (ch === delimiter && stack.length === 0) {
|
|
774
|
+
segments.push({
|
|
775
|
+
start: segmentStart,
|
|
776
|
+
end: i,
|
|
777
|
+
text: code.slice(segmentStart, i),
|
|
778
|
+
});
|
|
779
|
+
segmentStart = i + 1;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
segments.push({
|
|
784
|
+
start: segmentStart,
|
|
785
|
+
end,
|
|
786
|
+
text: code.slice(segmentStart, end),
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
return segments;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Find the innermost unmatched `(` before the cursor.
|
|
794
|
+
* Only returns it when the cursor is at top level inside that call.
|
|
795
|
+
*
|
|
796
|
+
* @param {string} code
|
|
797
|
+
* @param {number} cursor
|
|
798
|
+
* @returns {number|null}
|
|
799
|
+
*/
|
|
800
|
+
function findActiveCallOpenParen(code, cursor) {
|
|
801
|
+
const stack = [];
|
|
802
|
+
let stringQuote = null;
|
|
803
|
+
let lineComment = false;
|
|
804
|
+
let blockComment = false;
|
|
805
|
+
|
|
806
|
+
for (let i = 0; i < cursor; i++) {
|
|
807
|
+
const ch = code[i];
|
|
808
|
+
const next = i + 1 < cursor ? code[i + 1] : '';
|
|
809
|
+
|
|
810
|
+
if (lineComment) {
|
|
811
|
+
if (ch === '\n') lineComment = false;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (blockComment) {
|
|
816
|
+
if (ch === '*' && next === '/') {
|
|
817
|
+
blockComment = false;
|
|
818
|
+
i++;
|
|
819
|
+
}
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (stringQuote) {
|
|
824
|
+
if (ch === stringQuote && !isEscapedAt(code, i)) {
|
|
825
|
+
stringQuote = null;
|
|
826
|
+
}
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (ch === '#' || (ch === '/' && next === '/')) {
|
|
831
|
+
lineComment = true;
|
|
832
|
+
if (ch === '/') i++;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (ch === '/' && next === '*') {
|
|
837
|
+
blockComment = true;
|
|
838
|
+
i++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
843
|
+
stringQuote = ch;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
848
|
+
stack.push({ ch, index: i });
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
853
|
+
const expected = ch === ')' ? '(' : ch === ']' ? '[' : '{';
|
|
854
|
+
if (stack[stack.length - 1]?.ch === expected) {
|
|
855
|
+
stack.pop();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const top = stack[stack.length - 1];
|
|
861
|
+
return top?.ch === '(' ? top.index : null;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Extract a simple callable expression before an opening parenthesis.
|
|
866
|
+
* Supports names like `foo` and dotted paths like `obj.method`.
|
|
867
|
+
*
|
|
868
|
+
* @param {string} code
|
|
869
|
+
* @param {number} openParen
|
|
870
|
+
* @returns {string|null}
|
|
871
|
+
*/
|
|
872
|
+
function extractCallableNameBeforeParen(code, openParen) {
|
|
873
|
+
const prefix = code.slice(0, openParen);
|
|
874
|
+
const match = prefix.match(/([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*$/);
|
|
875
|
+
return match?.[1] || null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Extract a leading keyword argument name from an argument segment.
|
|
880
|
+
*
|
|
881
|
+
* @param {string} text
|
|
882
|
+
* @returns {string|null}
|
|
883
|
+
*/
|
|
884
|
+
function extractAssignedKeywordName(text) {
|
|
885
|
+
const match = text.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
886
|
+
return match?.[1] || null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Determine whether the current argument segment is empty or a simple identifier prefix.
|
|
891
|
+
*
|
|
892
|
+
* @param {{start: number, end: number, text: string}} segment
|
|
893
|
+
* @returns {{prefix: string, start: number, end: number}|null}
|
|
894
|
+
*/
|
|
895
|
+
function getArgumentPrefixInfo(segment) {
|
|
896
|
+
const text = segment.text;
|
|
897
|
+
|
|
898
|
+
if (!text.trim()) {
|
|
899
|
+
return {
|
|
900
|
+
prefix: '',
|
|
901
|
+
start: segment.end,
|
|
902
|
+
end: segment.end,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const match = text.match(/([A-Za-z_][A-Za-z0-9_]*)\s*$/);
|
|
907
|
+
if (!match) return null;
|
|
908
|
+
|
|
909
|
+
const before = text.slice(0, match.index);
|
|
910
|
+
if (before.trim()) return null;
|
|
911
|
+
|
|
912
|
+
return {
|
|
913
|
+
prefix: match[1],
|
|
914
|
+
start: segment.start + match.index,
|
|
915
|
+
end: segment.end,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Analyze the active call context at the cursor position.
|
|
921
|
+
*
|
|
922
|
+
* @param {string} code
|
|
923
|
+
* @param {number} cursor
|
|
924
|
+
* @returns {{callee: string, openParen: number, segments: Array<{start: number, end: number, text: string}>, currentSegment: {start: number, end: number, text: string}, usedKeywords: Set<string>, activeKeyword: string|null, positionalIndex: number}|null}
|
|
925
|
+
*/
|
|
926
|
+
function getActiveCallContext(code, cursor) {
|
|
927
|
+
const openParen = findActiveCallOpenParen(code, cursor);
|
|
928
|
+
if (openParen == null) return null;
|
|
929
|
+
|
|
930
|
+
const callee = extractCallableNameBeforeParen(code, openParen);
|
|
931
|
+
if (!callee) return null;
|
|
932
|
+
|
|
933
|
+
const segments = splitTopLevelRange(code, openParen + 1, cursor);
|
|
934
|
+
const currentSegment = segments[segments.length - 1] || {
|
|
935
|
+
start: cursor,
|
|
936
|
+
end: cursor,
|
|
937
|
+
text: '',
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const usedKeywords = new Set();
|
|
941
|
+
let positionalIndex = 0;
|
|
942
|
+
|
|
943
|
+
for (const segment of segments.slice(0, -1)) {
|
|
944
|
+
const text = segment.text.trim();
|
|
945
|
+
if (!text) continue;
|
|
946
|
+
|
|
947
|
+
const keywordName = extractAssignedKeywordName(segment.text);
|
|
948
|
+
if (keywordName) {
|
|
949
|
+
usedKeywords.add(keywordName);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (/^\*{1,2}/.test(text)) continue;
|
|
954
|
+
positionalIndex++;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
callee,
|
|
959
|
+
openParen,
|
|
960
|
+
segments,
|
|
961
|
+
currentSegment,
|
|
962
|
+
usedKeywords,
|
|
963
|
+
activeKeyword: extractAssignedKeywordName(currentSegment.text),
|
|
964
|
+
positionalIndex,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Find call-argument completion context for the current cursor position.
|
|
970
|
+
*
|
|
971
|
+
* @param {string} code
|
|
972
|
+
* @param {number} cursor
|
|
973
|
+
* @returns {{callee: string, openParen: number, prefix: string, replaceStart: number, replaceEnd: number, usedKeywords: Set<string>}|null}
|
|
974
|
+
*/
|
|
975
|
+
function getCallArgumentContext(code, cursor) {
|
|
976
|
+
const activeCall = getActiveCallContext(code, cursor);
|
|
977
|
+
if (!activeCall) return null;
|
|
978
|
+
|
|
979
|
+
if (/^\s*\*{1,2}/.test(activeCall.currentSegment.text)) return null;
|
|
980
|
+
if (activeCall.activeKeyword) return null;
|
|
981
|
+
|
|
982
|
+
const prefixInfo = getArgumentPrefixInfo(activeCall.currentSegment);
|
|
983
|
+
if (!prefixInfo) return null;
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
callee: activeCall.callee,
|
|
987
|
+
openParen: activeCall.openParen,
|
|
988
|
+
prefix: prefixInfo.prefix,
|
|
989
|
+
replaceStart: prefixInfo.start,
|
|
990
|
+
replaceEnd: prefixInfo.end,
|
|
991
|
+
usedKeywords: activeCall.usedKeywords,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Parse a signature string into keyword-capable parameters.
|
|
997
|
+
*
|
|
998
|
+
* @param {string} signature
|
|
999
|
+
* @returns {Array<{name: string, declaration: string, required: boolean}>}
|
|
1000
|
+
*/
|
|
1001
|
+
function parseSignatureParameters(signature) {
|
|
1002
|
+
if (!signature) return [];
|
|
1003
|
+
|
|
1004
|
+
const openParen = signature.indexOf('(');
|
|
1005
|
+
const closeParen = signature.lastIndexOf(')');
|
|
1006
|
+
if (openParen < 0 || closeParen <= openParen) return [];
|
|
1007
|
+
|
|
1008
|
+
const inner = signature.slice(openParen + 1, closeParen);
|
|
1009
|
+
const parts = splitTopLevelRange(inner, 0, inner.length).map(part => part.text.trim());
|
|
1010
|
+
if (parts.length === 0) return [];
|
|
1011
|
+
|
|
1012
|
+
const positionalOnlyEnd = parts.indexOf('/');
|
|
1013
|
+
|
|
1014
|
+
/** @type {Array<{name: string, declaration: string, required: boolean}>} */
|
|
1015
|
+
const parameters = [];
|
|
1016
|
+
|
|
1017
|
+
for (let index = 0; index < parts.length; index++) {
|
|
1018
|
+
const part = parts[index];
|
|
1019
|
+
if (!part || part === '/' || part === '*') continue;
|
|
1020
|
+
if (positionalOnlyEnd !== -1 && index < positionalOnlyEnd) continue;
|
|
1021
|
+
if (part.startsWith('**')) continue;
|
|
1022
|
+
if (part.startsWith('*')) continue;
|
|
1023
|
+
|
|
1024
|
+
const nameMatch = part.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
|
|
1025
|
+
const name = nameMatch?.[1];
|
|
1026
|
+
if (!name || name === 'self' || name === 'cls') continue;
|
|
1027
|
+
|
|
1028
|
+
parameters.push({
|
|
1029
|
+
name,
|
|
1030
|
+
declaration: part,
|
|
1031
|
+
required: !part.includes('='),
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return parameters;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Extract parameter docs from common docstring formats.
|
|
1040
|
+
*
|
|
1041
|
+
* @param {string} docs
|
|
1042
|
+
* @param {string} parameterName
|
|
1043
|
+
* @returns {string}
|
|
1044
|
+
*/
|
|
1045
|
+
function extractParameterDocumentation(docs, parameterName) {
|
|
1046
|
+
const text = cleanDocsText(docs);
|
|
1047
|
+
if (!text) return '';
|
|
1048
|
+
|
|
1049
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
1050
|
+
|
|
1051
|
+
// NumPy-style docstrings
|
|
1052
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
1053
|
+
if (!/^\s*Parameters\s*$/i.test(lines[i]) || !/^\s*-{3,}\s*$/.test(lines[i + 1])) continue;
|
|
1054
|
+
|
|
1055
|
+
let currentNames = [];
|
|
1056
|
+
/** @type {string[]} */
|
|
1057
|
+
let currentLines = [];
|
|
1058
|
+
|
|
1059
|
+
const flush = () => {
|
|
1060
|
+
if (currentNames.includes(parameterName)) {
|
|
1061
|
+
return currentLines.join('\n').trim();
|
|
1062
|
+
}
|
|
1063
|
+
return '';
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
1067
|
+
const line = lines[j];
|
|
1068
|
+
|
|
1069
|
+
if (/^\S/.test(line) && j + 1 < lines.length && /^\s*-{3,}\s*$/.test(lines[j + 1])) {
|
|
1070
|
+
return flush();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const headerMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)(\s*:.*)?\s*$/);
|
|
1074
|
+
if (headerMatch && !/^\s/.test(line)) {
|
|
1075
|
+
const existing = flush();
|
|
1076
|
+
if (existing) return existing;
|
|
1077
|
+
|
|
1078
|
+
currentNames = headerMatch[1].split(/\s*,\s*/);
|
|
1079
|
+
currentLines = [];
|
|
1080
|
+
const typeInfo = headerMatch[2]?.replace(/^\s*:\s*/, '').trim();
|
|
1081
|
+
if (typeInfo) currentLines.push(typeInfo);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (currentNames.length > 0) {
|
|
1086
|
+
currentLines.push(line.trimEnd());
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return flush();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Google-style docstrings
|
|
1094
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1095
|
+
if (!/^\s*(Args|Arguments|Parameters)\s*:\s*$/i.test(lines[i])) continue;
|
|
1096
|
+
|
|
1097
|
+
let currentName = '';
|
|
1098
|
+
/** @type {string[]} */
|
|
1099
|
+
let currentLines = [];
|
|
1100
|
+
|
|
1101
|
+
const flush = () => {
|
|
1102
|
+
if (currentName === parameterName) {
|
|
1103
|
+
return currentLines.join('\n').trim();
|
|
1104
|
+
}
|
|
1105
|
+
return '';
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1109
|
+
const line = lines[j];
|
|
1110
|
+
if (!line.trim()) {
|
|
1111
|
+
if (currentName) currentLines.push('');
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (/^\S/.test(line) && !/^\s{2,}/.test(line)) {
|
|
1116
|
+
return flush();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const headerMatch = line.match(/^\s{2,}([A-Za-z_][A-Za-z0-9_]*)(?:\s*\([^)]*\))?\s*:\s*(.*)$/);
|
|
1120
|
+
if (headerMatch) {
|
|
1121
|
+
const existing = flush();
|
|
1122
|
+
if (existing) return existing;
|
|
1123
|
+
|
|
1124
|
+
currentName = headerMatch[1];
|
|
1125
|
+
currentLines = headerMatch[2] ? [headerMatch[2].trim()] : [];
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (currentName) {
|
|
1130
|
+
currentLines.push(line.trim());
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return flush();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return '';
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Create a short fallback summary from a docstring.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {string} docs
|
|
1144
|
+
* @returns {string}
|
|
1145
|
+
*/
|
|
1146
|
+
function summarizeDocs(docs) {
|
|
1147
|
+
const text = cleanDocsText(docs);
|
|
1148
|
+
if (!text) return '';
|
|
1149
|
+
|
|
1150
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
1151
|
+
const summary = [];
|
|
1152
|
+
|
|
1153
|
+
for (const line of lines) {
|
|
1154
|
+
if (!line.trim()) {
|
|
1155
|
+
if (summary.length > 0) break;
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
summary.push(line.trim());
|
|
1159
|
+
if (summary.length >= 5) break;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return summary.join('\n').trim();
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Get the first non-empty line from a doc snippet.
|
|
1167
|
+
*
|
|
1168
|
+
* @param {string} docs
|
|
1169
|
+
* @param {number} [maxLength=88]
|
|
1170
|
+
* @returns {string}
|
|
1171
|
+
*/
|
|
1172
|
+
function firstDocLine(docs, maxLength = 88) {
|
|
1173
|
+
const text = cleanDocsText(docs);
|
|
1174
|
+
if (!text) return '';
|
|
1175
|
+
|
|
1176
|
+
const line = text
|
|
1177
|
+
.replace(/\r\n?/g, '\n')
|
|
1178
|
+
.split('\n')
|
|
1179
|
+
.map(part => part.trim())
|
|
1180
|
+
.find(Boolean) || '';
|
|
1181
|
+
|
|
1182
|
+
if (!line) return '';
|
|
1183
|
+
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Compact a parameter declaration for use in the completion row.
|
|
1188
|
+
*
|
|
1189
|
+
* @param {string} declaration
|
|
1190
|
+
* @param {string} name
|
|
1191
|
+
* @param {number} [maxLength=48]
|
|
1192
|
+
* @returns {string}
|
|
1193
|
+
*/
|
|
1194
|
+
function compactParameterDeclaration(declaration, name, maxLength = 48) {
|
|
1195
|
+
if (!declaration) return '';
|
|
1196
|
+
|
|
1197
|
+
let text = declaration
|
|
1198
|
+
.replace(new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*[:=]?\\s*`), '')
|
|
1199
|
+
.replace(/(['"])[^'"]{16,}\1/g, '$1…$1')
|
|
1200
|
+
.replace(/\s+/g, ' ')
|
|
1201
|
+
.trim();
|
|
1202
|
+
|
|
1203
|
+
if (!text) text = declaration.trim();
|
|
1204
|
+
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Shorten doc text for completion info / compact signature help.
|
|
1209
|
+
*
|
|
1210
|
+
* @param {string} docs
|
|
1211
|
+
* @param {number} [maxLines=4]
|
|
1212
|
+
* @returns {string}
|
|
1213
|
+
*/
|
|
1214
|
+
function compactDocs(docs, maxLines = 4) {
|
|
1215
|
+
const text = cleanDocsText(docs);
|
|
1216
|
+
if (!text) return '';
|
|
1217
|
+
|
|
1218
|
+
const lines = text
|
|
1219
|
+
.replace(/\r\n?/g, '\n')
|
|
1220
|
+
.split('\n')
|
|
1221
|
+
.map(line => line.trim())
|
|
1222
|
+
.filter((line, index, arr) => line || (index > 0 && arr[index - 1]));
|
|
1223
|
+
|
|
1224
|
+
const clipped = lines.slice(0, maxLines).join('\n').trim();
|
|
1225
|
+
return clipped;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Parse a signature for display and active-parameter highlighting.
|
|
1230
|
+
*
|
|
1231
|
+
* @param {string} signature
|
|
1232
|
+
* @returns {{head: string, tail: string, parts: Array<{text: string, name: string|null, kind: 'parameter'|'marker'|'varargs'|'kwargs', positionalCapable: boolean, keywordCapable: boolean}>}|null}
|
|
1233
|
+
*/
|
|
1234
|
+
function parseSignatureDisplay(signature) {
|
|
1235
|
+
if (!signature) return null;
|
|
1236
|
+
|
|
1237
|
+
const openParen = signature.indexOf('(');
|
|
1238
|
+
const closeParen = signature.lastIndexOf(')');
|
|
1239
|
+
if (openParen < 0 || closeParen <= openParen) return null;
|
|
1240
|
+
|
|
1241
|
+
const head = signature.slice(0, openParen + 1);
|
|
1242
|
+
const inner = signature.slice(openParen + 1, closeParen);
|
|
1243
|
+
const tail = signature.slice(closeParen);
|
|
1244
|
+
const rawParts = splitTopLevelRange(inner, 0, inner.length).map(part => part.text.trim());
|
|
1245
|
+
const positionalOnlyEnd = rawParts.indexOf('/');
|
|
1246
|
+
|
|
1247
|
+
/** @type {Array<{text: string, name: string|null, kind: 'parameter'|'marker'|'varargs'|'kwargs', positionalCapable: boolean, keywordCapable: boolean}>} */
|
|
1248
|
+
const parts = [];
|
|
1249
|
+
let keywordOnlyMode = false;
|
|
1250
|
+
|
|
1251
|
+
for (let index = 0; index < rawParts.length; index++) {
|
|
1252
|
+
const part = rawParts[index];
|
|
1253
|
+
if (!part) continue;
|
|
1254
|
+
|
|
1255
|
+
if (part === '/') {
|
|
1256
|
+
parts.push({
|
|
1257
|
+
text: part,
|
|
1258
|
+
name: null,
|
|
1259
|
+
kind: 'marker',
|
|
1260
|
+
positionalCapable: false,
|
|
1261
|
+
keywordCapable: false,
|
|
1262
|
+
});
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (part === '*') {
|
|
1267
|
+
keywordOnlyMode = true;
|
|
1268
|
+
parts.push({
|
|
1269
|
+
text: part,
|
|
1270
|
+
name: null,
|
|
1271
|
+
kind: 'marker',
|
|
1272
|
+
positionalCapable: false,
|
|
1273
|
+
keywordCapable: false,
|
|
1274
|
+
});
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const positionalOnly = positionalOnlyEnd !== -1 && index < positionalOnlyEnd;
|
|
1279
|
+
const isVarKeyword = part.startsWith('**');
|
|
1280
|
+
const isVarArgs = !isVarKeyword && part.startsWith('*');
|
|
1281
|
+
const nameMatch = part.match(/^\*{0,2}([A-Za-z_][A-Za-z0-9_]*)/);
|
|
1282
|
+
const name = nameMatch?.[1] || null;
|
|
1283
|
+
|
|
1284
|
+
parts.push({
|
|
1285
|
+
text: part,
|
|
1286
|
+
name,
|
|
1287
|
+
kind: isVarKeyword ? 'kwargs' : isVarArgs ? 'varargs' : 'parameter',
|
|
1288
|
+
positionalCapable: isVarArgs || (!keywordOnlyMode && !isVarKeyword),
|
|
1289
|
+
keywordCapable: !positionalOnly && !isVarArgs && !isVarKeyword,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
if (isVarArgs) {
|
|
1293
|
+
keywordOnlyMode = true;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return { head, tail, parts };
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Resolve the active signature part for the current call context.
|
|
1302
|
+
*
|
|
1303
|
+
* @param {{parts: Array<{text: string, name: string|null, kind: 'parameter'|'marker'|'varargs'|'kwargs', positionalCapable: boolean, keywordCapable: boolean}>}} parsedSignature
|
|
1304
|
+
* @param {{currentSegment: {text: string}, usedKeywords: Set<string>, activeKeyword: string|null, positionalIndex: number}} callContext
|
|
1305
|
+
* @returns {{text: string, name: string|null, kind: 'parameter'|'marker'|'varargs'|'kwargs', positionalCapable: boolean, keywordCapable: boolean}|null}
|
|
1306
|
+
*/
|
|
1307
|
+
function resolveActiveSignaturePart(parsedSignature, callContext) {
|
|
1308
|
+
const trimmedCurrent = callContext.currentSegment.text.trimStart();
|
|
1309
|
+
|
|
1310
|
+
if (trimmedCurrent.startsWith('**')) {
|
|
1311
|
+
return parsedSignature.parts.find(part => part.kind === 'kwargs') || null;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (trimmedCurrent.startsWith('*')) {
|
|
1315
|
+
return parsedSignature.parts.find(part => part.kind === 'varargs') || null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (callContext.activeKeyword) {
|
|
1319
|
+
return parsedSignature.parts.find(part => part.name === callContext.activeKeyword) || null;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const positionalParts = parsedSignature.parts.filter(part => part.positionalCapable);
|
|
1323
|
+
if (callContext.positionalIndex < positionalParts.length) {
|
|
1324
|
+
return positionalParts[callContext.positionalIndex] || null;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const remainingKeywordPart = parsedSignature.parts.find(part =>
|
|
1328
|
+
part.keywordCapable && part.name && !callContext.usedKeywords.has(part.name)
|
|
1329
|
+
);
|
|
1330
|
+
if (remainingKeywordPart) return remainingKeywordPart;
|
|
1331
|
+
|
|
1332
|
+
return positionalParts[positionalParts.length - 1] || null;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Format a parsed signature to HTML with the active parameter highlighted.
|
|
1337
|
+
*
|
|
1338
|
+
* @param {{head: string, tail: string, parts: Array<{text: string, name: string|null, kind: 'parameter'|'marker'|'varargs'|'kwargs', positionalCapable: boolean, keywordCapable: boolean}>}} parsedSignature
|
|
1339
|
+
* @param {{text: string, name: string|null, kind: 'parameter'|'marker'|'varargs'|'kwargs', positionalCapable: boolean, keywordCapable: boolean}|null} activePart
|
|
1340
|
+
* @returns {string}
|
|
1341
|
+
*/
|
|
1342
|
+
function formatSignatureMarkup(parsedSignature, activePart) {
|
|
1343
|
+
const pieces = [`<span class="mrmd-signature-help__head">${escapeHtml(parsedSignature.head)}</span>`];
|
|
1344
|
+
|
|
1345
|
+
parsedSignature.parts.forEach((part, index) => {
|
|
1346
|
+
if (index > 0) {
|
|
1347
|
+
pieces.push('<span class="mrmd-signature-help__comma">, </span>');
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
const classes = ['mrmd-signature-help__part'];
|
|
1351
|
+
if (part.kind === 'marker') classes.push('mrmd-signature-help__part--marker');
|
|
1352
|
+
if (activePart === part) classes.push('mrmd-signature-help__part--active');
|
|
1353
|
+
|
|
1354
|
+
pieces.push(`<span class="${classes.join(' ')}">${escapeHtml(part.text)}</span>`);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
pieces.push(`<span class="mrmd-signature-help__tail">${escapeHtml(parsedSignature.tail)}</span>`);
|
|
1358
|
+
return pieces.join('');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Format signature help content as HTML.
|
|
1363
|
+
*
|
|
1364
|
+
* @param {InspectResult} inspectResult
|
|
1365
|
+
* @param {{currentSegment: {text: string}, usedKeywords: Set<string>, activeKeyword: string|null, positionalIndex: number}} callContext
|
|
1366
|
+
* @param {{compact?: boolean}} [options]
|
|
1367
|
+
* @returns {string}
|
|
1368
|
+
*/
|
|
1369
|
+
function formatSignatureHelpContent(inspectResult, callContext, { compact = false } = {}) {
|
|
1370
|
+
const parsedSignature = parseSignatureDisplay(inspectResult.signature || '');
|
|
1371
|
+
if (!parsedSignature) return '';
|
|
1372
|
+
|
|
1373
|
+
const activePart = resolveActiveSignaturePart(parsedSignature, callContext);
|
|
1374
|
+
const docs = inspectResult.documentation || inspectResult.docstring || '';
|
|
1375
|
+
const parameterDocs = activePart?.name ? extractParameterDocumentation(docs, activePart.name) : '';
|
|
1376
|
+
const fallbackDocs = compactDocs(summarizeDocs(docs), compact ? 2 : 4);
|
|
1377
|
+
|
|
1378
|
+
let html = `<div class="mrmd-signature-help${compact ? ' mrmd-signature-help--compact' : ''}">`;
|
|
1379
|
+
html += `<div class="mrmd-signature-help__signature"><code>${formatSignatureMarkup(parsedSignature, activePart)}</code></div>`;
|
|
1380
|
+
|
|
1381
|
+
if (activePart && activePart.kind !== 'marker') {
|
|
1382
|
+
html += `<div class="mrmd-signature-help__active">${escapeHtml(activePart.text)}</div>`;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (!compact && inspectResult.name) {
|
|
1386
|
+
html += `<div class="mrmd-signature-help__name">${escapeHtml(inspectResult.name)}</div>`;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const docsText = compact ? firstDocLine(parameterDocs || fallbackDocs, 72) : (parameterDocs || fallbackDocs);
|
|
1390
|
+
if (docsText) {
|
|
1391
|
+
html += `<div class="mrmd-signature-help__docs">${escapeHtml(docsText)}</div>`;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
html += '</div>';
|
|
1395
|
+
return html;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Retrieve signature-bearing inspection data, falling back to hover when inspect is unavailable
|
|
1400
|
+
* or returns too little information.
|
|
1401
|
+
*
|
|
1402
|
+
* @param {Object} options
|
|
1403
|
+
* @param {RuntimeLSPProvider} options.provider
|
|
1404
|
+
* @param {string} options.code
|
|
1405
|
+
* @param {number} options.cursor
|
|
1406
|
+
* @param {string} options.language
|
|
1407
|
+
* @returns {Promise<InspectResult|null>}
|
|
1408
|
+
*/
|
|
1409
|
+
async function getSignatureInspectData({ provider, code, cursor, language }) {
|
|
1410
|
+
let inspectResult = null;
|
|
1411
|
+
|
|
1412
|
+
if (provider.inspect) {
|
|
1413
|
+
try {
|
|
1414
|
+
inspectResult = await provider.inspect(code, cursor, language, { detail: 1 });
|
|
1415
|
+
} catch {
|
|
1416
|
+
inspectResult = null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
let hoverResult = null;
|
|
1421
|
+
const needsHoverFallback = !inspectResult?.signature || (!inspectResult.documentation && !inspectResult.docstring);
|
|
1422
|
+
if (needsHoverFallback && provider.hover) {
|
|
1423
|
+
try {
|
|
1424
|
+
hoverResult = await provider.hover(code, cursor, language);
|
|
1425
|
+
} catch {
|
|
1426
|
+
hoverResult = null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const signature = inspectResult?.signature || hoverResult?.signature;
|
|
1431
|
+
if (!signature) return null;
|
|
1432
|
+
|
|
1433
|
+
return {
|
|
1434
|
+
...(inspectResult || {}),
|
|
1435
|
+
found: true,
|
|
1436
|
+
name: inspectResult?.name || hoverResult?.name,
|
|
1437
|
+
type: inspectResult?.type || hoverResult?.type,
|
|
1438
|
+
signature,
|
|
1439
|
+
documentation:
|
|
1440
|
+
inspectResult?.documentation ||
|
|
1441
|
+
inspectResult?.docstring ||
|
|
1442
|
+
hoverResult?.documentation ||
|
|
1443
|
+
hoverResult?.docstring,
|
|
1444
|
+
docstring: inspectResult?.docstring || hoverResult?.docstring,
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Get an anchor rect derived from the autocomplete popup, for VS Code-like stacked placement.
|
|
1450
|
+
*
|
|
1451
|
+
* @param {import('@codemirror/view').EditorView} view
|
|
1452
|
+
* @returns {import('@codemirror/view').Rect|null}
|
|
1453
|
+
*/
|
|
1454
|
+
function getAutocompleteTooltipRect(view) {
|
|
1455
|
+
const popup = view.dom.querySelector('.cm-tooltip-autocomplete');
|
|
1456
|
+
if (!(popup instanceof HTMLElement)) return null;
|
|
1457
|
+
|
|
1458
|
+
const rect = popup.getBoundingClientRect();
|
|
1459
|
+
return {
|
|
1460
|
+
left: rect.left,
|
|
1461
|
+
right: rect.left + 1,
|
|
1462
|
+
top: rect.top,
|
|
1463
|
+
bottom: rect.top,
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Lazily inspect a completion candidate to provide docs in the side info panel.
|
|
321
1469
|
*
|
|
322
|
-
* @param {
|
|
323
|
-
* @param {
|
|
324
|
-
* @
|
|
1470
|
+
* @param {Object} options
|
|
1471
|
+
* @param {RuntimeLSPProvider} options.provider
|
|
1472
|
+
* @param {string} options.code
|
|
1473
|
+
* @param {number} options.cursorStart
|
|
1474
|
+
* @param {number} options.cursorEnd
|
|
1475
|
+
* @param {string} options.insertText
|
|
1476
|
+
* @param {string} options.language
|
|
1477
|
+
* @returns {Promise<Partial<CompletionItem>|null>}
|
|
325
1478
|
*/
|
|
326
|
-
function
|
|
327
|
-
|
|
328
|
-
|
|
1479
|
+
async function loadCompletionItemInfo({ provider, code, cursorStart, cursorEnd, insertText, language }) {
|
|
1480
|
+
if (!provider.inspect && !provider.hover) return null;
|
|
1481
|
+
|
|
1482
|
+
const candidateCode = `${code.slice(0, cursorStart)}${insertText}${code.slice(cursorEnd)}`;
|
|
1483
|
+
const candidateCursor = cursorStart + insertText.length;
|
|
1484
|
+
|
|
1485
|
+
let inspectResult = null;
|
|
1486
|
+
if (provider.inspect) {
|
|
1487
|
+
try {
|
|
1488
|
+
inspectResult = await provider.inspect(candidateCode, candidateCursor, language, { detail: 1 });
|
|
1489
|
+
} catch {
|
|
1490
|
+
inspectResult = null;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
329
1493
|
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
1494
|
+
let hoverResult = null;
|
|
1495
|
+
const needsHover = !inspectResult?.found || (!inspectResult.signature && !inspectResult.documentation && !inspectResult.docstring);
|
|
1496
|
+
if (needsHover && provider.hover) {
|
|
1497
|
+
try {
|
|
1498
|
+
hoverResult = await provider.hover(candidateCode, candidateCursor, language);
|
|
1499
|
+
} catch {
|
|
1500
|
+
hoverResult = null;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const signature = inspectResult?.signature || hoverResult?.signature || '';
|
|
1505
|
+
const type = inspectResult?.type || hoverResult?.type || '';
|
|
1506
|
+
const docs =
|
|
1507
|
+
inspectResult?.documentation ||
|
|
1508
|
+
inspectResult?.docstring ||
|
|
1509
|
+
hoverResult?.documentation ||
|
|
1510
|
+
hoverResult?.docstring ||
|
|
1511
|
+
hoverResult?.value || '';
|
|
1512
|
+
|
|
1513
|
+
const detail = signature || type || '';
|
|
1514
|
+
if (!detail && !docs) return null;
|
|
333
1515
|
|
|
334
1516
|
return {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
cell,
|
|
1517
|
+
label: inspectResult?.name || hoverResult?.name,
|
|
1518
|
+
detail,
|
|
1519
|
+
documentation: docs,
|
|
339
1520
|
};
|
|
340
1521
|
}
|
|
341
1522
|
|
|
342
|
-
// #endregion LANGUAGE_DETECTION
|
|
343
|
-
|
|
344
|
-
// #region HOVER_EXTENSION
|
|
345
|
-
|
|
346
1523
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
* Shows actual runtime values when hovering over variables/symbols.
|
|
1524
|
+
* Build high-priority keyword argument completions for the active call site.
|
|
350
1525
|
*
|
|
351
1526
|
* @param {Object} options
|
|
352
|
-
* @param {
|
|
353
|
-
* @param {
|
|
354
|
-
* @param {
|
|
355
|
-
* @param {
|
|
356
|
-
* @returns {
|
|
1527
|
+
* @param {RuntimeLSPProvider} options.provider
|
|
1528
|
+
* @param {string} options.code
|
|
1529
|
+
* @param {string} options.language
|
|
1530
|
+
* @param {{callee: string, openParen: number, prefix: string, replaceStart: number, replaceEnd: number, usedKeywords: Set<string>}} options.callContext
|
|
1531
|
+
* @returns {Promise<{matches: CompletionItem[], cursorStart: number, cursorEnd: number}|null>}
|
|
357
1532
|
*/
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// Find provider for this language
|
|
367
|
-
const provider = findProviderForLanguage(providers, codeInfo.language);
|
|
368
|
-
if (!provider) return null;
|
|
1533
|
+
async function getCallKeywordCompletions({ provider, code, language, callContext }) {
|
|
1534
|
+
const inspectResult = await getSignatureInspectData({
|
|
1535
|
+
provider,
|
|
1536
|
+
code,
|
|
1537
|
+
cursor: callContext.openParen,
|
|
1538
|
+
language,
|
|
1539
|
+
});
|
|
1540
|
+
if (!inspectResult?.signature) return null;
|
|
369
1541
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (!hoverResult || !hoverResult.found) return null;
|
|
1542
|
+
const parameters = parseSignatureParameters(inspectResult.signature);
|
|
1543
|
+
if (parameters.length === 0) return null;
|
|
373
1544
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const position = yText
|
|
377
|
-
? await import('yjs').then(Y => Y.createRelativePositionFromTypeIndex(yText, pos))
|
|
378
|
-
: { line: view.state.doc.lineAt(pos).number, ch: pos - view.state.doc.lineAt(pos).from };
|
|
1545
|
+
const lowerPrefix = callContext.prefix.toLowerCase();
|
|
1546
|
+
const docs = inspectResult.documentation || inspectResult.docstring || '';
|
|
379
1547
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1548
|
+
const matches = parameters
|
|
1549
|
+
.filter(param => !callContext.usedKeywords.has(param.name))
|
|
1550
|
+
.filter(param => !lowerPrefix || param.name.toLowerCase().startsWith(lowerPrefix))
|
|
1551
|
+
.map((param, index) => {
|
|
1552
|
+
const parameterDocs = extractParameterDocumentation(docs, param.name);
|
|
1553
|
+
const summary = firstDocLine(parameterDocs || summarizeDocs(docs));
|
|
1554
|
+
const info = [
|
|
1555
|
+
`Parameter: ${param.declaration}`,
|
|
1556
|
+
compactDocs(parameterDocs || summarizeDocs(docs), 6),
|
|
1557
|
+
].filter(Boolean).join('\n\n');
|
|
388
1558
|
|
|
389
|
-
// Create tooltip DOM
|
|
390
1559
|
return {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
},
|
|
1560
|
+
label: `${param.name}=`,
|
|
1561
|
+
insertText: `${param.name}=`,
|
|
1562
|
+
kind: 'field',
|
|
1563
|
+
detail: compactParameterDeclaration(param.declaration, param.name),
|
|
1564
|
+
documentation: info,
|
|
1565
|
+
boost: 10000 - index,
|
|
1566
|
+
sortText: String(index).padStart(4, '0'),
|
|
1567
|
+
valuePreview: summary || undefined,
|
|
400
1568
|
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
if (matches.length === 0) return null;
|
|
1572
|
+
|
|
1573
|
+
return {
|
|
1574
|
+
matches,
|
|
1575
|
+
cursorStart: callContext.replaceStart,
|
|
1576
|
+
cursorEnd: callContext.replaceEnd,
|
|
1577
|
+
};
|
|
407
1578
|
}
|
|
408
1579
|
|
|
409
1580
|
/**
|
|
410
|
-
*
|
|
1581
|
+
* Merge synthesized call-argument completions with runtime completions.
|
|
411
1582
|
*
|
|
412
|
-
* @param {
|
|
413
|
-
* @
|
|
1583
|
+
* @param {CompletionResult|null} runtimeResult
|
|
1584
|
+
* @param {{matches: CompletionItem[], cursorStart: number, cursorEnd: number}|null} callResult
|
|
1585
|
+
* @returns {{matches: CompletionItem[], cursorStart: number, cursorEnd: number}|null}
|
|
414
1586
|
*/
|
|
415
|
-
function
|
|
416
|
-
|
|
1587
|
+
function mergeCompletionResults(runtimeResult, callResult) {
|
|
1588
|
+
const runtimeMatches = runtimeResult?.matches || [];
|
|
1589
|
+
const callMatches = callResult?.matches || [];
|
|
417
1590
|
|
|
418
|
-
|
|
419
|
-
if (result.name) {
|
|
420
|
-
html += `<div class="mrmd-hover-name"><code>${escapeHtml(result.name)}</code>`;
|
|
421
|
-
if (result.type) {
|
|
422
|
-
html += ` <span class="mrmd-hover-type">${escapeHtml(result.type)}</span>`;
|
|
423
|
-
}
|
|
424
|
-
html += '</div>';
|
|
425
|
-
}
|
|
1591
|
+
if (runtimeMatches.length === 0 && callMatches.length === 0) return null;
|
|
426
1592
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
html += `<div class="mrmd-hover-signature"><code>${escapeHtml(result.signature)}</code></div>`;
|
|
430
|
-
}
|
|
1593
|
+
const seen = new Set();
|
|
1594
|
+
const matches = [];
|
|
431
1595
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
1596
|
+
for (const match of callMatches) {
|
|
1597
|
+
const key = `${match.label}\u0000${match.insertText || ''}`;
|
|
1598
|
+
if (seen.has(key)) continue;
|
|
1599
|
+
seen.add(key);
|
|
1600
|
+
matches.push(match);
|
|
435
1601
|
}
|
|
436
1602
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1603
|
+
for (const match of runtimeMatches) {
|
|
1604
|
+
const key = `${match.label}\u0000${match.insertText || ''}`;
|
|
1605
|
+
if (seen.has(key)) continue;
|
|
1606
|
+
seen.add(key);
|
|
1607
|
+
matches.push(match);
|
|
440
1608
|
}
|
|
441
1609
|
|
|
442
|
-
|
|
443
|
-
|
|
1610
|
+
return {
|
|
1611
|
+
matches,
|
|
1612
|
+
cursorStart: callResult?.cursorStart ?? runtimeResult?.cursorStart ?? 0,
|
|
1613
|
+
cursorEnd: callResult?.cursorEnd ?? runtimeResult?.cursorEnd ?? 0,
|
|
1614
|
+
};
|
|
444
1615
|
}
|
|
445
1616
|
|
|
446
1617
|
/**
|
|
447
|
-
*
|
|
1618
|
+
* Build a compact DOM info panel for a completion item.
|
|
448
1619
|
*
|
|
449
|
-
* @param {
|
|
450
|
-
* @returns {
|
|
1620
|
+
* @param {CompletionItem} match
|
|
1621
|
+
* @returns {((completion: any) => import('@codemirror/autocomplete').CompletionInfo)|undefined}
|
|
451
1622
|
*/
|
|
452
|
-
function
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
1623
|
+
function createCompletionInfoRenderer(match, loadInfo) {
|
|
1624
|
+
const render = (infoMatch) => {
|
|
1625
|
+
const docs = compactDocs(infoMatch.documentation || '', 6);
|
|
1626
|
+
const detail = infoMatch.detail || '';
|
|
1627
|
+
|
|
1628
|
+
if (!docs && !detail) return null;
|
|
1629
|
+
|
|
1630
|
+
const dom = document.createElement('div');
|
|
1631
|
+
dom.className = 'mrmd-completion-info';
|
|
1632
|
+
|
|
1633
|
+
const title = document.createElement('div');
|
|
1634
|
+
title.className = 'mrmd-completion-info__title';
|
|
1635
|
+
title.textContent = infoMatch.label;
|
|
1636
|
+
dom.appendChild(title);
|
|
1637
|
+
|
|
1638
|
+
if (detail) {
|
|
1639
|
+
const detailEl = document.createElement('div');
|
|
1640
|
+
detailEl.className = 'mrmd-completion-info__detail';
|
|
1641
|
+
detailEl.textContent = detail;
|
|
1642
|
+
dom.appendChild(detailEl);
|
|
1643
|
+
}
|
|
460
1644
|
|
|
461
|
-
|
|
1645
|
+
if (docs) {
|
|
1646
|
+
const docsEl = document.createElement('div');
|
|
1647
|
+
docsEl.className = 'mrmd-completion-info__docs';
|
|
1648
|
+
docsEl.textContent = docs;
|
|
1649
|
+
dom.appendChild(docsEl);
|
|
1650
|
+
}
|
|
462
1651
|
|
|
463
|
-
|
|
1652
|
+
return dom;
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
const immediate = render(match);
|
|
1656
|
+
if (immediate && !loadInfo) {
|
|
1657
|
+
return () => immediate.cloneNode(true);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (!loadInfo) return undefined;
|
|
1661
|
+
|
|
1662
|
+
return async () => {
|
|
1663
|
+
const loaded = await loadInfo();
|
|
1664
|
+
if (!loaded) return immediate ? immediate.cloneNode(true) : null;
|
|
1665
|
+
const dom = render({ ...match, ...loaded });
|
|
1666
|
+
return dom || (immediate ? immediate.cloneNode(true) : null);
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
464
1669
|
|
|
465
1670
|
/**
|
|
466
1671
|
* Create a CodeMirror completion source powered by runtime LSP.
|
|
@@ -486,8 +1691,23 @@ export function createRuntimeCompletionSource({ providers, getContent, stateMana
|
|
|
486
1691
|
const provider = findProviderForLanguage(providers, codeInfo.language);
|
|
487
1692
|
if (!provider) return null;
|
|
488
1693
|
|
|
489
|
-
|
|
490
|
-
|
|
1694
|
+
const callContext = getCallArgumentContext(codeInfo.code, codeInfo.offset);
|
|
1695
|
+
|
|
1696
|
+
let result;
|
|
1697
|
+
|
|
1698
|
+
// Inside a callable's argument-name position, only show synthesized keyword arguments.
|
|
1699
|
+
// This avoids noisy IPython globals/magics when the user has already opened `(` and is
|
|
1700
|
+
// clearly trying to pick a kwarg.
|
|
1701
|
+
if (callContext) {
|
|
1702
|
+
result = await getCallKeywordCompletions({
|
|
1703
|
+
provider,
|
|
1704
|
+
code: codeInfo.code,
|
|
1705
|
+
language: codeInfo.language,
|
|
1706
|
+
callContext,
|
|
1707
|
+
});
|
|
1708
|
+
} else {
|
|
1709
|
+
result = await provider.complete(codeInfo.code, codeInfo.offset, codeInfo.language);
|
|
1710
|
+
}
|
|
491
1711
|
if (!result || !result.matches || result.matches.length === 0) return null;
|
|
492
1712
|
|
|
493
1713
|
// Broadcast to awareness if available
|
|
@@ -516,12 +1736,24 @@ export function createRuntimeCompletionSource({ providers, getContent, stateMana
|
|
|
516
1736
|
options: result.matches.map(match => {
|
|
517
1737
|
const insertText = match.insertText || match.label;
|
|
518
1738
|
const shouldRetrigger = /[\/\.]$/.test(insertText);
|
|
1739
|
+
const infoLoader = (!match.documentation && (provider.inspect || provider.hover))
|
|
1740
|
+
? () => loadCompletionItemInfo({
|
|
1741
|
+
provider,
|
|
1742
|
+
code: codeInfo.code,
|
|
1743
|
+
cursorStart: result.cursorStart,
|
|
1744
|
+
cursorEnd: result.cursorEnd,
|
|
1745
|
+
insertText,
|
|
1746
|
+
language: codeInfo.language,
|
|
1747
|
+
})
|
|
1748
|
+
: null;
|
|
519
1749
|
|
|
520
1750
|
return {
|
|
521
1751
|
label: match.label,
|
|
522
1752
|
type: mapCompletionKind(match.kind),
|
|
523
|
-
detail: match.
|
|
524
|
-
info: match
|
|
1753
|
+
detail: match.detail || match.valuePreview,
|
|
1754
|
+
info: createCompletionInfoRenderer(match, infoLoader),
|
|
1755
|
+
section: match.section,
|
|
1756
|
+
sortText: match.sortText,
|
|
525
1757
|
// Use custom apply to retrigger completions for paths and chained access
|
|
526
1758
|
apply: shouldRetrigger
|
|
527
1759
|
? (view, completion, from, to) => {
|
|
@@ -533,7 +1765,9 @@ export function createRuntimeCompletionSource({ providers, getContent, stateMana
|
|
|
533
1765
|
setTimeout(() => startCompletion(view), 0);
|
|
534
1766
|
}
|
|
535
1767
|
: insertText,
|
|
536
|
-
boost:
|
|
1768
|
+
boost: typeof match.boost === 'number'
|
|
1769
|
+
? match.boost
|
|
1770
|
+
: (match.kind === 'property' || match.kind === 'method' ? 1 : 0),
|
|
537
1771
|
};
|
|
538
1772
|
}),
|
|
539
1773
|
};
|
|
@@ -590,6 +1824,225 @@ export function createRuntimeCompletionExtension({ providers, getContent, stateM
|
|
|
590
1824
|
|
|
591
1825
|
// #endregion COMPLETION_EXTENSION
|
|
592
1826
|
|
|
1827
|
+
// #region SIGNATURE_HELP
|
|
1828
|
+
|
|
1829
|
+
const setSignatureHelpTooltip = StateEffect.define();
|
|
1830
|
+
const clearSignatureHelpTooltip = StateEffect.define();
|
|
1831
|
+
|
|
1832
|
+
const signatureHelpTooltipField = StateField.define({
|
|
1833
|
+
create() {
|
|
1834
|
+
return null;
|
|
1835
|
+
},
|
|
1836
|
+
update(value, tr) {
|
|
1837
|
+
for (const effect of tr.effects) {
|
|
1838
|
+
if (effect.is(setSignatureHelpTooltip)) return effect.value;
|
|
1839
|
+
if (effect.is(clearSignatureHelpTooltip)) return null;
|
|
1840
|
+
}
|
|
1841
|
+
return value;
|
|
1842
|
+
},
|
|
1843
|
+
provide: f => showTooltip.from(f),
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Create a tooltip descriptor for signature help.
|
|
1848
|
+
*
|
|
1849
|
+
* @param {number} pos
|
|
1850
|
+
* @param {InspectResult} inspectResult
|
|
1851
|
+
* @param {{currentSegment: {text: string}, usedKeywords: Set<string>, activeKeyword: string|null, positionalIndex: number}} callContext
|
|
1852
|
+
* @param {{compact?: boolean}} [options]
|
|
1853
|
+
* @returns {import('@codemirror/view').Tooltip|null}
|
|
1854
|
+
*/
|
|
1855
|
+
function createSignatureHelpTooltipDescriptor(pos, inspectResult, callContext, { compact = false } = {}) {
|
|
1856
|
+
const html = formatSignatureHelpContent(inspectResult, callContext, { compact });
|
|
1857
|
+
if (!html) return null;
|
|
1858
|
+
|
|
1859
|
+
return {
|
|
1860
|
+
pos,
|
|
1861
|
+
above: compact,
|
|
1862
|
+
strictSide: false,
|
|
1863
|
+
arrow: false,
|
|
1864
|
+
clip: false,
|
|
1865
|
+
create(view) {
|
|
1866
|
+
const dom = document.createElement('div');
|
|
1867
|
+
dom.className = 'mrmd-runtime-signature-help';
|
|
1868
|
+
dom.innerHTML = html;
|
|
1869
|
+
|
|
1870
|
+
return {
|
|
1871
|
+
dom,
|
|
1872
|
+
offset: { x: 0, y: compact ? 6 : 14 },
|
|
1873
|
+
overlap: compact,
|
|
1874
|
+
getCoords: compact
|
|
1875
|
+
? () => getAutocompleteTooltipRect(view) || view.coordsAtPos(pos)
|
|
1876
|
+
: undefined,
|
|
1877
|
+
positioned: () => {
|
|
1878
|
+
if (!compact) {
|
|
1879
|
+
dom.style.width = '';
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const popup = view.dom.querySelector('.cm-tooltip-autocomplete');
|
|
1884
|
+
if (popup instanceof HTMLElement) {
|
|
1885
|
+
const width = Math.min(Math.max(popup.offsetWidth - 12, 220), 420);
|
|
1886
|
+
dom.style.width = `${width}px`;
|
|
1887
|
+
} else {
|
|
1888
|
+
dom.style.width = '';
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
1891
|
+
};
|
|
1892
|
+
},
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Create a CodeMirror signature-help extension powered by runtime LSP inspect.
|
|
1898
|
+
*
|
|
1899
|
+
* Shows the active callable signature while the cursor is inside its argument list.
|
|
1900
|
+
*
|
|
1901
|
+
* @param {Object} options
|
|
1902
|
+
* @param {Map<string, RuntimeLSPProvider>} options.providers
|
|
1903
|
+
* @param {function(): string} options.getContent
|
|
1904
|
+
* @param {Object} [options.config]
|
|
1905
|
+
* @param {number} [options.config.debounceMs=80]
|
|
1906
|
+
* @returns {import('@codemirror/state').Extension}
|
|
1907
|
+
*/
|
|
1908
|
+
export function createRuntimeSignatureHelpExtension({ providers, getContent, config = {} }) {
|
|
1909
|
+
const debounceMs = config.debounceMs ?? 80;
|
|
1910
|
+
|
|
1911
|
+
const plugin = ViewPlugin.fromClass(class {
|
|
1912
|
+
constructor(view) {
|
|
1913
|
+
this.view = view;
|
|
1914
|
+
this.requestId = 0;
|
|
1915
|
+
this.timer = null;
|
|
1916
|
+
this.destroyed = false;
|
|
1917
|
+
this.cachedInspectKey = null;
|
|
1918
|
+
this.cachedInspectResult = null;
|
|
1919
|
+
this.autocompleteOpen = false;
|
|
1920
|
+
this.schedule();
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
update(update) {
|
|
1924
|
+
const autocompleteOpen = !!update.view.dom.querySelector('.cm-tooltip-autocomplete');
|
|
1925
|
+
if (update.docChanged || update.selectionSet || update.focusChanged || autocompleteOpen !== this.autocompleteOpen) {
|
|
1926
|
+
this.autocompleteOpen = autocompleteOpen;
|
|
1927
|
+
this.schedule();
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
destroy() {
|
|
1932
|
+
this.destroyed = true;
|
|
1933
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
schedule() {
|
|
1937
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1938
|
+
this.timer = setTimeout(() => {
|
|
1939
|
+
this.timer = null;
|
|
1940
|
+
this.refresh();
|
|
1941
|
+
}, debounceMs);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
clear() {
|
|
1945
|
+
if (this.destroyed) return;
|
|
1946
|
+
if (!this.view.state.field(signatureHelpTooltipField, false)) return;
|
|
1947
|
+
this.view.dispatch({ effects: clearSignatureHelpTooltip.of(null) });
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
async refresh() {
|
|
1951
|
+
if (this.destroyed) return;
|
|
1952
|
+
|
|
1953
|
+
const selection = this.view.state.selection.main;
|
|
1954
|
+
if (!selection.empty) {
|
|
1955
|
+
this.clear();
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const content = getContent();
|
|
1960
|
+
const pos = selection.head;
|
|
1961
|
+
const codeInfo = getCodeAtPosition(content, pos);
|
|
1962
|
+
if (!codeInfo) {
|
|
1963
|
+
this.clear();
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
const provider = findProviderForLanguage(providers, codeInfo.language);
|
|
1968
|
+
if (!provider?.inspect && !provider?.hover) {
|
|
1969
|
+
this.clear();
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const callContext = getActiveCallContext(codeInfo.code, codeInfo.offset);
|
|
1974
|
+
if (!callContext) {
|
|
1975
|
+
this.clear();
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const inspectKey = `${codeInfo.language}:${codeInfo.cell.start}:${callContext.openParen}:${callContext.callee}`;
|
|
1980
|
+
let inspectResult = this.cachedInspectKey === inspectKey ? this.cachedInspectResult : null;
|
|
1981
|
+
|
|
1982
|
+
const requestId = ++this.requestId;
|
|
1983
|
+
if (!inspectResult) {
|
|
1984
|
+
inspectResult = await getSignatureInspectData({
|
|
1985
|
+
provider,
|
|
1986
|
+
code: codeInfo.code,
|
|
1987
|
+
cursor: callContext.openParen,
|
|
1988
|
+
language: codeInfo.language,
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
if (this.destroyed || requestId !== this.requestId) return;
|
|
1992
|
+
|
|
1993
|
+
if (!inspectResult?.found || !inspectResult.signature) {
|
|
1994
|
+
this.cachedInspectKey = null;
|
|
1995
|
+
this.cachedInspectResult = null;
|
|
1996
|
+
this.clear();
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
this.cachedInspectKey = inspectKey;
|
|
2001
|
+
this.cachedInspectResult = inspectResult;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const latestSelection = this.view.state.selection.main;
|
|
2005
|
+
if (!latestSelection.empty) {
|
|
2006
|
+
this.clear();
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const latestContent = getContent();
|
|
2011
|
+
const latestPos = latestSelection.head;
|
|
2012
|
+
const latestCodeInfo = getCodeAtPosition(latestContent, latestPos);
|
|
2013
|
+
const latestCallContext = latestCodeInfo
|
|
2014
|
+
? getActiveCallContext(latestCodeInfo.code, latestCodeInfo.offset)
|
|
2015
|
+
: null;
|
|
2016
|
+
const latestInspectKey = latestCodeInfo && latestCallContext
|
|
2017
|
+
? `${latestCodeInfo.language}:${latestCodeInfo.cell.start}:${latestCallContext.openParen}:${latestCallContext.callee}`
|
|
2018
|
+
: null;
|
|
2019
|
+
|
|
2020
|
+
if (!latestCodeInfo || !latestCallContext || latestInspectKey !== inspectKey) {
|
|
2021
|
+
this.schedule();
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
const autocompleteOpen = this.autocompleteOpen || !!this.view.dom.querySelector('.cm-tooltip-autocomplete');
|
|
2026
|
+
const tooltip = createSignatureHelpTooltipDescriptor(
|
|
2027
|
+
latestPos,
|
|
2028
|
+
inspectResult,
|
|
2029
|
+
latestCallContext,
|
|
2030
|
+
{ compact: autocompleteOpen }
|
|
2031
|
+
);
|
|
2032
|
+
if (!tooltip) {
|
|
2033
|
+
this.clear();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
this.view.dispatch({ effects: setSignatureHelpTooltip.of(tooltip) });
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
return [signatureHelpTooltipField, plugin];
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// #endregion SIGNATURE_HELP
|
|
2045
|
+
|
|
593
2046
|
// #region VARIABLE_EXPLORER
|
|
594
2047
|
|
|
595
2048
|
/**
|
|
@@ -722,14 +2175,22 @@ function getCellIndex(content, cell) {
|
|
|
722
2175
|
export const runtimeLspStyles = `
|
|
723
2176
|
/* Runtime Hover Tooltip */
|
|
724
2177
|
.mrmd-runtime-hover {
|
|
725
|
-
background: var(--
|
|
726
|
-
border: 1px solid var(--
|
|
727
|
-
border-radius: 6px;
|
|
2178
|
+
background: var(--widget-surface-elevated, var(--editor-background, #1e1e1e));
|
|
2179
|
+
border: 1px solid var(--widget-border, #333);
|
|
2180
|
+
border-radius: var(--widget-border-radius, 6px);
|
|
728
2181
|
padding: 8px 12px;
|
|
729
|
-
max-width:
|
|
2182
|
+
max-width: 460px;
|
|
2183
|
+
max-height: min(52vh, 440px);
|
|
2184
|
+
overflow: auto;
|
|
730
2185
|
font-size: 13px;
|
|
731
|
-
line-height: 1.
|
|
732
|
-
|
|
2186
|
+
line-height: 1.45;
|
|
2187
|
+
color: var(--widget-text, var(--editor-foreground, #e1e1e1));
|
|
2188
|
+
box-shadow: var(--mrmd-shadow-md, 0 6px 18px rgba(0, 0, 0, 0.3));
|
|
2189
|
+
user-select: text;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
.mrmd-runtime-hover-sticky {
|
|
2193
|
+
border-color: var(--widget-border-focus, var(--mrmd-accent, #58a6ff));
|
|
733
2194
|
}
|
|
734
2195
|
|
|
735
2196
|
.mrmd-hover-content {
|
|
@@ -738,26 +2199,54 @@ export const runtimeLspStyles = `
|
|
|
738
2199
|
gap: 6px;
|
|
739
2200
|
}
|
|
740
2201
|
|
|
2202
|
+
.mrmd-hover-header {
|
|
2203
|
+
display: flex;
|
|
2204
|
+
align-items: center;
|
|
2205
|
+
justify-content: space-between;
|
|
2206
|
+
gap: 10px;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
741
2209
|
.mrmd-hover-name {
|
|
742
2210
|
font-weight: 600;
|
|
743
2211
|
}
|
|
744
2212
|
|
|
2213
|
+
.mrmd-hover-copy {
|
|
2214
|
+
border: 1px solid var(--widget-border, #333);
|
|
2215
|
+
background: var(--widget-surface, rgba(0, 0, 0, 0.2));
|
|
2216
|
+
color: var(--widget-text-muted, #9ca3af);
|
|
2217
|
+
border-radius: 4px;
|
|
2218
|
+
padding: 2px 8px;
|
|
2219
|
+
font-size: 11px;
|
|
2220
|
+
line-height: 1.2;
|
|
2221
|
+
cursor: pointer;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
.mrmd-hover-copy:hover {
|
|
2225
|
+
color: var(--widget-text, #e5e7eb);
|
|
2226
|
+
background: var(--widget-surface-hover, rgba(255, 255, 255, 0.08));
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
.mrmd-hover-copy:active {
|
|
2230
|
+
transform: translateY(1px);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
745
2233
|
.mrmd-hover-name code {
|
|
746
|
-
color: var(--
|
|
2234
|
+
color: var(--syntax-variable, var(--widget-text, #e1e1e1));
|
|
747
2235
|
background: none;
|
|
748
2236
|
padding: 0;
|
|
2237
|
+
font-family: var(--widget-font-mono, monospace);
|
|
749
2238
|
}
|
|
750
2239
|
|
|
751
2240
|
.mrmd-hover-type {
|
|
752
|
-
color: var(--
|
|
2241
|
+
color: var(--syntax-type, #4ec9b0);
|
|
753
2242
|
font-size: 12px;
|
|
754
2243
|
font-weight: normal;
|
|
755
2244
|
margin-left: 8px;
|
|
756
2245
|
}
|
|
757
2246
|
|
|
758
2247
|
.mrmd-hover-signature {
|
|
759
|
-
color: var(--
|
|
760
|
-
font-family: monospace;
|
|
2248
|
+
color: var(--syntax-function, #dcdcaa);
|
|
2249
|
+
font-family: var(--widget-font-mono, monospace);
|
|
761
2250
|
font-size: 12px;
|
|
762
2251
|
}
|
|
763
2252
|
|
|
@@ -767,53 +2256,170 @@ export const runtimeLspStyles = `
|
|
|
767
2256
|
}
|
|
768
2257
|
|
|
769
2258
|
.mrmd-hover-value {
|
|
770
|
-
color: var(--
|
|
771
|
-
font-family: monospace;
|
|
2259
|
+
color: var(--syntax-string, var(--widget-text, #ce9178));
|
|
2260
|
+
font-family: var(--widget-font-mono, monospace);
|
|
772
2261
|
font-size: 12px;
|
|
773
|
-
max-height:
|
|
2262
|
+
max-height: 120px;
|
|
774
2263
|
overflow: auto;
|
|
775
2264
|
white-space: pre-wrap;
|
|
776
2265
|
word-break: break-word;
|
|
777
2266
|
}
|
|
778
2267
|
|
|
779
2268
|
.mrmd-hover-docs {
|
|
780
|
-
color: var(--
|
|
2269
|
+
color: var(--widget-text-muted, #9cdcfe);
|
|
781
2270
|
font-size: 12px;
|
|
782
|
-
border-top: 1px solid var(--
|
|
2271
|
+
border-top: 1px solid var(--widget-border, #333);
|
|
783
2272
|
padding-top: 6px;
|
|
784
2273
|
margin-top: 2px;
|
|
2274
|
+
white-space: pre-wrap;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
.mrmd-hover-source {
|
|
2278
|
+
color: var(--widget-text-muted, #9ca3af);
|
|
2279
|
+
font-size: 11px;
|
|
2280
|
+
border-top: 1px dashed var(--widget-border, #333);
|
|
2281
|
+
padding-top: 6px;
|
|
2282
|
+
margin-top: 2px;
|
|
2283
|
+
word-break: break-all;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
.mrmd-hover-source-link {
|
|
2287
|
+
color: var(--syntax-link, var(--widget-text-accent, #58a6ff));
|
|
2288
|
+
text-decoration: underline dotted;
|
|
2289
|
+
text-underline-offset: 2px;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
.mrmd-hover-source-link:hover {
|
|
2293
|
+
text-decoration-style: solid;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/* Autocomplete / completion info */
|
|
2297
|
+
.cm-tooltip.cm-tooltip-autocomplete {
|
|
2298
|
+
margin-top: 8px;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
.cm-tooltip.cm-completionInfo {
|
|
2302
|
+
margin-left: 8px;
|
|
2303
|
+
max-width: 320px;
|
|
2304
|
+
max-height: 220px;
|
|
2305
|
+
overflow: auto;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
.mrmd-completion-info {
|
|
2309
|
+
display: flex;
|
|
2310
|
+
flex-direction: column;
|
|
2311
|
+
gap: 6px;
|
|
2312
|
+
max-width: 300px;
|
|
2313
|
+
font-size: 12px;
|
|
2314
|
+
line-height: 1.4;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
.mrmd-completion-info__title {
|
|
2318
|
+
color: var(--syntax-variable, var(--widget-text, #e1e1e1));
|
|
2319
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2320
|
+
font-weight: 600;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
.mrmd-completion-info__detail {
|
|
2324
|
+
color: var(--syntax-parameter, #9cdcfe);
|
|
2325
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2326
|
+
font-size: 11px;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.mrmd-completion-info__docs {
|
|
2330
|
+
color: var(--widget-text-muted, #9ca3af);
|
|
2331
|
+
white-space: pre-wrap;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/* Runtime Signature Help */
|
|
2335
|
+
.mrmd-runtime-signature-help {
|
|
2336
|
+
background: var(--widget-surface-elevated, var(--editor-background, #1e1e1e));
|
|
2337
|
+
border: 1px solid var(--widget-border, #333);
|
|
2338
|
+
border-radius: var(--widget-border-radius, 6px);
|
|
2339
|
+
padding: 5px 8px;
|
|
2340
|
+
max-width: 440px;
|
|
2341
|
+
color: var(--widget-text, var(--editor-foreground, #e1e1e1));
|
|
2342
|
+
box-shadow: var(--mrmd-shadow-md, 0 6px 18px rgba(0, 0, 0, 0.28));
|
|
2343
|
+
font-size: 11px;
|
|
2344
|
+
line-height: 1.35;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
.mrmd-signature-help {
|
|
2348
|
+
display: flex;
|
|
2349
|
+
flex-direction: column;
|
|
2350
|
+
gap: 4px;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
.mrmd-signature-help--compact {
|
|
2354
|
+
gap: 2px;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
.mrmd-signature-help__signature {
|
|
2358
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2359
|
+
color: var(--widget-text, var(--editor-foreground, #e1e1e1));
|
|
2360
|
+
white-space: pre-wrap;
|
|
2361
|
+
word-break: break-word;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
.mrmd-signature-help--compact .mrmd-signature-help__signature {
|
|
2365
|
+
white-space: nowrap;
|
|
2366
|
+
overflow: hidden;
|
|
2367
|
+
text-overflow: ellipsis;
|
|
2368
|
+
max-width: 100%;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
.mrmd-signature-help__signature code {
|
|
2372
|
+
background: none;
|
|
2373
|
+
padding: 0;
|
|
785
2374
|
}
|
|
786
2375
|
|
|
787
|
-
|
|
788
|
-
.
|
|
789
|
-
:
|
|
790
|
-
background: #ffffff;
|
|
791
|
-
border-color: #e0e0e0;
|
|
792
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
2376
|
+
.mrmd-signature-help__head,
|
|
2377
|
+
.mrmd-signature-help__tail {
|
|
2378
|
+
color: var(--syntax-function, #dcdcaa);
|
|
793
2379
|
}
|
|
794
2380
|
|
|
795
|
-
.
|
|
796
|
-
:
|
|
797
|
-
color: #1e1e1e;
|
|
2381
|
+
.mrmd-signature-help__part {
|
|
2382
|
+
color: var(--widget-text, var(--editor-foreground, #e1e1e1));
|
|
798
2383
|
}
|
|
799
2384
|
|
|
800
|
-
.
|
|
801
|
-
|
|
802
|
-
color: #
|
|
2385
|
+
.mrmd-signature-help__part--marker,
|
|
2386
|
+
.mrmd-signature-help__comma {
|
|
2387
|
+
color: var(--widget-text-muted, #9ca3af);
|
|
803
2388
|
}
|
|
804
2389
|
|
|
805
|
-
.
|
|
806
|
-
:
|
|
807
|
-
|
|
2390
|
+
.mrmd-signature-help__part--active {
|
|
2391
|
+
color: var(--editor-background, #111827);
|
|
2392
|
+
background: var(--mrmd-accent, #58a6ff);
|
|
2393
|
+
border-radius: 4px;
|
|
2394
|
+
padding: 1px 4px;
|
|
808
2395
|
}
|
|
809
2396
|
|
|
810
|
-
.
|
|
811
|
-
:
|
|
812
|
-
|
|
813
|
-
|
|
2397
|
+
.mrmd-signature-help__active {
|
|
2398
|
+
color: var(--syntax-parameter, #9cdcfe);
|
|
2399
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2400
|
+
font-size: 11px;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
.mrmd-signature-help__name {
|
|
2404
|
+
color: var(--widget-text-muted, #9ca3af);
|
|
2405
|
+
font-size: 11px;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
.mrmd-signature-help__docs {
|
|
2409
|
+
color: var(--widget-text-muted, #9cdcfe);
|
|
2410
|
+
border-top: 1px solid var(--widget-border, #333);
|
|
2411
|
+
padding-top: 4px;
|
|
2412
|
+
white-space: pre-wrap;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
.mrmd-signature-help--compact .mrmd-signature-help__docs {
|
|
2416
|
+
border-top: 0;
|
|
2417
|
+
padding-top: 0;
|
|
2418
|
+
color: var(--widget-text-muted, #9ca3af);
|
|
814
2419
|
}
|
|
815
2420
|
`;
|
|
816
2421
|
|
|
2422
|
+
|
|
817
2423
|
let stylesInjected = false;
|
|
818
2424
|
|
|
819
2425
|
/**
|
|
@@ -842,6 +2448,7 @@ export default {
|
|
|
842
2448
|
createRuntimeHoverExtension,
|
|
843
2449
|
createRuntimeCompletionSource,
|
|
844
2450
|
createRuntimeCompletionExtension,
|
|
2451
|
+
createRuntimeSignatureHelpExtension,
|
|
845
2452
|
|
|
846
2453
|
// Variable Explorer
|
|
847
2454
|
createVariableExplorer,
|