@tuidom/elements 0.1.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/dist/body/bodyElement.d.ts +33 -0
- package/dist/body/bodyElement.js +87 -0
- package/dist/button/buttonElement.d.ts +18 -0
- package/dist/button/buttonElement.js +68 -0
- package/dist/completionlist/completionDetailsElement.d.ts +49 -0
- package/dist/completionlist/completionDetailsElement.js +165 -0
- package/dist/completionlist/completionItemKindIcon.d.ts +2 -0
- package/dist/completionlist/completionItemKindIcon.js +43 -0
- package/dist/completionlist/completionListElement.d.ts +102 -0
- package/dist/completionlist/completionListElement.js +309 -0
- package/dist/completionlist/completionWidgetElement.d.ts +41 -0
- package/dist/completionlist/completionWidgetElement.js +102 -0
- package/dist/contextview/contextMenuController.d.ts +23 -0
- package/dist/contextview/contextMenuController.js +75 -0
- package/dist/contextview/overlayHostElement.d.ts +23 -0
- package/dist/contextview/overlayHostElement.js +47 -0
- package/dist/editorgroup/editorTabItemElement.d.ts +41 -0
- package/dist/editorgroup/editorTabItemElement.js +186 -0
- package/dist/editorgroup/editorTabStripElement.d.ts +33 -0
- package/dist/editorgroup/editorTabStripElement.js +118 -0
- package/dist/editorpart/editorPartElement.d.ts +95 -0
- package/dist/editorpart/editorPartElement.js +285 -0
- package/dist/inputbox/inputElement.d.ts +34 -0
- package/dist/inputbox/inputElement.js +207 -0
- package/dist/inputbox/inputState.d.ts +69 -0
- package/dist/inputbox/inputState.js +326 -0
- package/dist/layout/boxContainerElement.d.ts +27 -0
- package/dist/layout/boxContainerElement.js +121 -0
- package/dist/layout/boxElement.d.ts +4 -0
- package/dist/layout/boxElement.js +30 -0
- package/dist/layout/fillerElement.d.ts +12 -0
- package/dist/layout/fillerElement.js +16 -0
- package/dist/layout/fitContentElement.d.ts +18 -0
- package/dist/layout/fitContentElement.js +46 -0
- package/dist/layout/hFlexElement.d.ts +26 -0
- package/dist/layout/hFlexElement.js +134 -0
- package/dist/layout/paddingContainerElement.d.ts +30 -0
- package/dist/layout/paddingContainerElement.js +88 -0
- package/dist/layout/sizedBoxElement.d.ts +30 -0
- package/dist/layout/sizedBoxElement.js +74 -0
- package/dist/layout/vFlexElement.d.ts +32 -0
- package/dist/layout/vFlexElement.js +136 -0
- package/dist/layout/vStackElement.d.ts +18 -0
- package/dist/layout/vStackElement.js +74 -0
- package/dist/list/listViewElement.d.ts +232 -0
- package/dist/list/listViewElement.js +944 -0
- package/dist/menu/menuBarElement.d.ts +47 -0
- package/dist/menu/menuBarElement.js +285 -0
- package/dist/menu/menuBarItemElement.d.ts +20 -0
- package/dist/menu/menuBarItemElement.js +68 -0
- package/dist/menu/popupMenuElement.d.ts +81 -0
- package/dist/menu/popupMenuElement.js +318 -0
- package/dist/menu/popupMenuItemElement.d.ts +24 -0
- package/dist/menu/popupMenuItemElement.js +89 -0
- package/dist/panel/panelContainerElement.d.ts +64 -0
- package/dist/panel/panelContainerElement.js +219 -0
- package/dist/quickpick/quickPickElement.d.ts +161 -0
- package/dist/quickpick/quickPickElement.js +561 -0
- package/dist/sash/sashElement.d.ts +30 -0
- package/dist/sash/sashElement.js +78 -0
- package/dist/scrollbar/iScrollable.d.ts +8 -0
- package/dist/scrollbar/iScrollable.js +1 -0
- package/dist/scrollbar/scrollBarRenderer.d.ts +36 -0
- package/dist/scrollbar/scrollBarRenderer.js +76 -0
- package/dist/scrollbar/scrollContainerElement.d.ts +39 -0
- package/dist/scrollbar/scrollContainerElement.js +91 -0
- package/dist/scrollbar/scrollViewport.d.ts +28 -0
- package/dist/scrollbar/scrollViewport.js +65 -0
- package/dist/scrollbar/scrollableElement.d.ts +22 -0
- package/dist/scrollbar/scrollableElement.js +42 -0
- package/dist/selectbox/selectBoxElement.d.ts +48 -0
- package/dist/selectbox/selectBoxElement.js +161 -0
- package/dist/terminal/encodeKeyForPty.d.ts +3 -0
- package/dist/terminal/encodeKeyForPty.js +56 -0
- package/dist/terminal/terminalViewElement.d.ts +17 -0
- package/dist/terminal/terminalViewElement.js +172 -0
- package/dist/text/textBlockElement.d.ts +13 -0
- package/dist/text/textBlockElement.js +35 -0
- package/dist/text/textLabelElement.d.ts +28 -0
- package/dist/text/textLabelElement.js +77 -0
- package/dist/titledpanel/titledPanelElement.d.ts +14 -0
- package/dist/titledpanel/titledPanelElement.js +40 -0
- package/dist/tree/iTreeDataProvider.d.ts +19 -0
- package/dist/tree/iTreeDataProvider.js +1 -0
- package/dist/tree/treeViewElement.d.ts +116 -0
- package/dist/tree/treeViewElement.js +748 -0
- package/dist/workbenchlayout/workbenchLayoutElement.d.ts +60 -0
- package/dist/workbenchlayout/workbenchLayoutElement.js +203 -0
- package/package.json +33 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { OverlayLayer } from "@tuidom/core/dom/overlayLayer";
|
|
3
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
export declare class BodyElement extends TUIElement {
|
|
5
|
+
private titleValue;
|
|
6
|
+
readonly overlayLayer: OverlayLayer;
|
|
7
|
+
get title(): string;
|
|
8
|
+
set title(value: string);
|
|
9
|
+
private readonly vflex;
|
|
10
|
+
/**
|
|
11
|
+
* Держит место контента, пока тот не задан: без fill-ряда между menuBar и
|
|
12
|
+
* statusBar статус-бар прилипал бы к верху, а не к нижнему ряду. Ничего не
|
|
13
|
+
* рисует (дефолтный render пустого элемента).
|
|
14
|
+
*/
|
|
15
|
+
private readonly contentSpacer;
|
|
16
|
+
private menuBar;
|
|
17
|
+
private content;
|
|
18
|
+
private statusBar;
|
|
19
|
+
constructor();
|
|
20
|
+
getOverlayLayer(): OverlayLayer;
|
|
21
|
+
setContent(element: TUIElement): void;
|
|
22
|
+
/** Слот занимает ровно 1 ряд — виджет, рисующий ниже ряда 0, будет обрезан. */
|
|
23
|
+
setMenuBar(menuBar: TUIElement | null): void;
|
|
24
|
+
/** Слот занимает ровно 1 ряд — виджет, рисующий ниже ряда 0, будет обрезан. */
|
|
25
|
+
setStatusBar(statusBar: TUIElement | null): void;
|
|
26
|
+
/**
|
|
27
|
+
* Пересобирает ряды vflex в каноническом порядке слотов: menuBar (1 ряд),
|
|
28
|
+
* content (остаток), statusBar (1 ряд).
|
|
29
|
+
*/
|
|
30
|
+
private syncSlots;
|
|
31
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
32
|
+
render(context: RenderContext): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { OverlayLayer } from "@tuidom/core/dom/overlayLayer";
|
|
3
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
import { VFlexElement, vflexFill, vflexFixed } from "../layout/vFlexElement.js";
|
|
5
|
+
export class BodyElement extends TUIElement {
|
|
6
|
+
titleValue = "";
|
|
7
|
+
overlayLayer;
|
|
8
|
+
get title() {
|
|
9
|
+
return this.titleValue;
|
|
10
|
+
}
|
|
11
|
+
set title(value) {
|
|
12
|
+
if (value === this.titleValue)
|
|
13
|
+
return;
|
|
14
|
+
this.titleValue = value;
|
|
15
|
+
// Заголовок — paint-состояние: кадр ввода рендерится только по
|
|
16
|
+
// dirty-флагу, мутация обязана помечать его сама.
|
|
17
|
+
this.markDirty();
|
|
18
|
+
}
|
|
19
|
+
vflex = new VFlexElement();
|
|
20
|
+
/**
|
|
21
|
+
* Держит место контента, пока тот не задан: без fill-ряда между menuBar и
|
|
22
|
+
* statusBar статус-бар прилипал бы к верху, а не к нижнему ряду. Ничего не
|
|
23
|
+
* рисует (дефолтный render пустого элемента).
|
|
24
|
+
*/
|
|
25
|
+
contentSpacer = new TUIElement();
|
|
26
|
+
menuBar = null;
|
|
27
|
+
content = null;
|
|
28
|
+
statusBar = null;
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
// BodyElement is the root, so mark it as such
|
|
32
|
+
this.setAsRoot();
|
|
33
|
+
this.overlayLayer = new OverlayLayer();
|
|
34
|
+
// Дети body неизменны; порядок — это z-порядок хит-теста:
|
|
35
|
+
// overlay последний, т.е. поверх всего.
|
|
36
|
+
this.setChildren([this.vflex, this.overlayLayer]);
|
|
37
|
+
}
|
|
38
|
+
getOverlayLayer() {
|
|
39
|
+
return this.overlayLayer;
|
|
40
|
+
}
|
|
41
|
+
setContent(element) {
|
|
42
|
+
this.content = element;
|
|
43
|
+
this.syncSlots();
|
|
44
|
+
}
|
|
45
|
+
/** Слот занимает ровно 1 ряд — виджет, рисующий ниже ряда 0, будет обрезан. */
|
|
46
|
+
setMenuBar(menuBar) {
|
|
47
|
+
this.menuBar = menuBar;
|
|
48
|
+
this.syncSlots();
|
|
49
|
+
}
|
|
50
|
+
/** Слот занимает ровно 1 ряд — виджет, рисующий ниже ряда 0, будет обрезан. */
|
|
51
|
+
setStatusBar(statusBar) {
|
|
52
|
+
this.statusBar = statusBar;
|
|
53
|
+
this.syncSlots();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Пересобирает ряды vflex в каноническом порядке слотов: menuBar (1 ряд),
|
|
57
|
+
* content (остаток), statusBar (1 ряд).
|
|
58
|
+
*/
|
|
59
|
+
syncSlots() {
|
|
60
|
+
const rows = [];
|
|
61
|
+
if (this.menuBar) {
|
|
62
|
+
this.menuBar.layoutStyle = { height: vflexFixed(1), width: "fill" };
|
|
63
|
+
rows.push(this.menuBar);
|
|
64
|
+
}
|
|
65
|
+
const contentRow = this.content ?? this.contentSpacer;
|
|
66
|
+
contentRow.layoutStyle = { height: vflexFill(), width: "fill" };
|
|
67
|
+
rows.push(contentRow);
|
|
68
|
+
if (this.statusBar) {
|
|
69
|
+
this.statusBar.layoutStyle = { height: vflexFixed(1), width: "fill" };
|
|
70
|
+
rows.push(this.statusBar);
|
|
71
|
+
}
|
|
72
|
+
this.vflex.replaceChildren(rows);
|
|
73
|
+
}
|
|
74
|
+
performLayout(constraints) {
|
|
75
|
+
const containerSize = super.performLayout(constraints);
|
|
76
|
+
this.layoutChild(this.vflex, 0, 0, BoxConstraints.tight(containerSize));
|
|
77
|
+
this.layoutChild(this.overlayLayer, 0, 0, BoxConstraints.tight(containerSize));
|
|
78
|
+
return containerSize;
|
|
79
|
+
}
|
|
80
|
+
render(context) {
|
|
81
|
+
// Title (legacy behaviour)
|
|
82
|
+
for (let y = 0; y < this.title.length; y++) {
|
|
83
|
+
context.setCell(y, 0, { char: this.title[y] });
|
|
84
|
+
}
|
|
85
|
+
this.renderChildren(context);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import type { TUIEventBase } from "@tuidom/core/dom/events/tuiEventBase";
|
|
3
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
export declare class ButtonElement extends TUIElement {
|
|
5
|
+
onActivate?: () => void;
|
|
6
|
+
private readonly label;
|
|
7
|
+
constructor(label: string);
|
|
8
|
+
getLabel(): string;
|
|
9
|
+
/** Режим «включённого тумблера» (кнопки-переключатели поиска): primary-вид. */
|
|
10
|
+
setChecked(checked: boolean): void;
|
|
11
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
12
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
13
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
14
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
15
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
16
|
+
protected performDefaultAction(event: TUIEventBase): void;
|
|
17
|
+
render(context: RenderContext): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { TUIKeyboardEvent } from "@tuidom/core/dom/events/tuiKeyboardEvent";
|
|
3
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
export class ButtonElement extends TUIElement {
|
|
5
|
+
onActivate;
|
|
6
|
+
label;
|
|
7
|
+
constructor(label) {
|
|
8
|
+
super();
|
|
9
|
+
this.label = label;
|
|
10
|
+
this.focusable = true;
|
|
11
|
+
// Вся state-логика цвета — в when-вариантах: hover/focus ставит ядро,
|
|
12
|
+
// "checked" (тумблеры) — setChecked. Порядок = приоритет: checked
|
|
13
|
+
// объявлен ПОСЛЕ focus — включённый тумблер выглядит primary
|
|
14
|
+
// независимо от фокуса.
|
|
15
|
+
this.style = {
|
|
16
|
+
fg: "button.secondaryForeground",
|
|
17
|
+
bg: "button.secondaryBackground",
|
|
18
|
+
when: [
|
|
19
|
+
{ states: ["hover"], bg: "button.secondaryHoverBackground" },
|
|
20
|
+
{ states: ["focus"], fg: "button.foreground", bg: "button.background" },
|
|
21
|
+
{ states: ["focus", "hover"], bg: "button.hoverBackground" },
|
|
22
|
+
{ states: ["checked"], fg: "button.foreground", bg: "button.background" },
|
|
23
|
+
{ states: ["checked", "hover"], bg: "button.hoverBackground" },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
this.addEventListener("click", (event) => {
|
|
27
|
+
if (event.defaultPrevented)
|
|
28
|
+
return;
|
|
29
|
+
this.onActivate?.();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
getLabel() {
|
|
33
|
+
return this.label;
|
|
34
|
+
}
|
|
35
|
+
/** Режим «включённого тумблера» (кнопки-переключатели поиска): primary-вид. */
|
|
36
|
+
setChecked(checked) {
|
|
37
|
+
this.setStyleState("checked", checked);
|
|
38
|
+
}
|
|
39
|
+
getMinIntrinsicWidth(_height) {
|
|
40
|
+
return this.label.length + 4;
|
|
41
|
+
}
|
|
42
|
+
getMaxIntrinsicWidth(_height) {
|
|
43
|
+
return this.label.length + 4;
|
|
44
|
+
}
|
|
45
|
+
getMinIntrinsicHeight(_width) {
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
getMaxIntrinsicHeight(_width) {
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
performLayout(constraints) {
|
|
52
|
+
const natural = new Size(this.getMaxIntrinsicWidth(1), this.getMaxIntrinsicHeight(0));
|
|
53
|
+
return super.performLayout(BoxConstraints.tight(constraints.constrain(natural)));
|
|
54
|
+
}
|
|
55
|
+
performDefaultAction(event) {
|
|
56
|
+
if (event.type !== "keydown")
|
|
57
|
+
return;
|
|
58
|
+
const keyEvent = event;
|
|
59
|
+
if (keyEvent.key === "Enter" || keyEvent.key === " ") {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
this.onActivate?.();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
render(context) {
|
|
65
|
+
const { fg, bg } = this.resolvedStyle;
|
|
66
|
+
context.drawText(0, 0, `[ ${this.label} ]`, { fg, bg }, { maxWidth: this.layoutSize.width });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
+
/** Содержимое панели: сигнатура сверху, документация под ней. */
|
|
4
|
+
export interface CompletionDetailsContent {
|
|
5
|
+
/** Сигнатура/тип пункта (`detail` или `labelDetails.detail`). */
|
|
6
|
+
readonly detail?: string;
|
|
7
|
+
/** Документация пункта. Markdown НЕ парсится — рендерера markdown нет. */
|
|
8
|
+
readonly documentation?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Панель описания выбранного пункта автодополнения — соседка
|
|
12
|
+
* {@link import("./completionListElement.js").CompletionListElement} в
|
|
13
|
+
* {@link import("./completionWidgetElement.js").CompletionWidgetElement}.
|
|
14
|
+
*
|
|
15
|
+
* Показывает сигнатуру и документацию с переносом по словам. Содержимое
|
|
16
|
+
* приходит извне уже догруженным (у LSP-источников описание доезжает отдельным
|
|
17
|
+
* `resolve`-запросом), сама панель ничего не запрашивает. Фокус не забирает —
|
|
18
|
+
* как и список, живёт при активном редакторе.
|
|
19
|
+
*/
|
|
20
|
+
export declare class CompletionDetailsElement extends TUIElement {
|
|
21
|
+
/** Предельная ширина панели; высоту ограничивает владелец по высоте списка. */
|
|
22
|
+
maxWidth: number;
|
|
23
|
+
maxHeight: number;
|
|
24
|
+
private detailValue;
|
|
25
|
+
private documentationValue;
|
|
26
|
+
/** Кэш переноса: (ширина текста) → строки. Layout зовут на каждый кадр. */
|
|
27
|
+
private wrapCache;
|
|
28
|
+
constructor();
|
|
29
|
+
/** Есть ли что показывать (пустая панель не занимает места). */
|
|
30
|
+
get isEmpty(): boolean;
|
|
31
|
+
setContent(content: CompletionDetailsContent | null): void;
|
|
32
|
+
/** Строки контента после переноса — для тестов и измерения. */
|
|
33
|
+
linesFor(textWidth: number): readonly string[];
|
|
34
|
+
private get textWidth();
|
|
35
|
+
private get boxWidth();
|
|
36
|
+
private get boxHeight();
|
|
37
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
38
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
39
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
40
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
41
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
42
|
+
render(context: RenderContext): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Перенос по словам в ширину `width`. Переводы строк источника сохраняются;
|
|
46
|
+
* слово длиннее строки режется по ширине (усечения контента не происходит —
|
|
47
|
+
* остаток уезжает на следующую строку).
|
|
48
|
+
*/
|
|
49
|
+
export declare function wrapText(text: string, width: number): string[];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { DisplayLine } from "@tuidom/core/common/displayLine";
|
|
2
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
3
|
+
import { truncateEnd } from "@tuidom/core/common/textTruncation";
|
|
4
|
+
import { BORDER_THICKNESS } from "@tuidom/core/dom/borderStyle";
|
|
5
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
6
|
+
// ─── Layout ───────────────────────────────────────────────────────────────────
|
|
7
|
+
// [│(0)][pad(1)][текст…][pad(w-2)][│(w-1)]
|
|
8
|
+
const TEXT_X = 2;
|
|
9
|
+
const RIGHT_PAD = 1;
|
|
10
|
+
const MIN_WIDTH = 20;
|
|
11
|
+
const DEFAULT_MAX_WIDTH = 60;
|
|
12
|
+
/**
|
|
13
|
+
* Панель описания выбранного пункта автодополнения — соседка
|
|
14
|
+
* {@link import("./completionListElement.ts").CompletionListElement} в
|
|
15
|
+
* {@link import("./completionWidgetElement.ts").CompletionWidgetElement}.
|
|
16
|
+
*
|
|
17
|
+
* Показывает сигнатуру и документацию с переносом по словам. Содержимое
|
|
18
|
+
* приходит извне уже догруженным (у LSP-источников описание доезжает отдельным
|
|
19
|
+
* `resolve`-запросом), сама панель ничего не запрашивает. Фокус не забирает —
|
|
20
|
+
* как и список, живёт при активном редакторе.
|
|
21
|
+
*/
|
|
22
|
+
export class CompletionDetailsElement extends TUIElement {
|
|
23
|
+
/** Предельная ширина панели; высоту ограничивает владелец по высоте списка. */
|
|
24
|
+
maxWidth = DEFAULT_MAX_WIDTH;
|
|
25
|
+
maxHeight = 10;
|
|
26
|
+
detailValue = "";
|
|
27
|
+
documentationValue = "";
|
|
28
|
+
/** Кэш переноса: (ширина текста) → строки. Layout зовут на каждый кадр. */
|
|
29
|
+
wrapCache = null;
|
|
30
|
+
constructor() {
|
|
31
|
+
super();
|
|
32
|
+
this.focusable = false;
|
|
33
|
+
}
|
|
34
|
+
/** Есть ли что показывать (пустая панель не занимает места). */
|
|
35
|
+
get isEmpty() {
|
|
36
|
+
return this.detailValue === "" && this.documentationValue === "";
|
|
37
|
+
}
|
|
38
|
+
setContent(content) {
|
|
39
|
+
const detail = content?.detail ?? "";
|
|
40
|
+
const documentation = content?.documentation ?? "";
|
|
41
|
+
if (detail === this.detailValue && documentation === this.documentationValue)
|
|
42
|
+
return;
|
|
43
|
+
this.detailValue = detail;
|
|
44
|
+
this.documentationValue = documentation;
|
|
45
|
+
this.wrapCache = null;
|
|
46
|
+
this.markDirty();
|
|
47
|
+
}
|
|
48
|
+
/** Строки контента после переноса — для тестов и измерения. */
|
|
49
|
+
linesFor(textWidth) {
|
|
50
|
+
if (this.wrapCache !== null && this.wrapCache.width === textWidth)
|
|
51
|
+
return this.wrapCache.lines;
|
|
52
|
+
const lines = [];
|
|
53
|
+
if (this.detailValue !== "")
|
|
54
|
+
lines.push(...wrapText(this.detailValue, textWidth));
|
|
55
|
+
// Пустая строка-разделитель нужна, только если есть обе части.
|
|
56
|
+
if (this.detailValue !== "" && this.documentationValue !== "")
|
|
57
|
+
lines.push("");
|
|
58
|
+
if (this.documentationValue !== "")
|
|
59
|
+
lines.push(...wrapText(this.documentationValue, textWidth));
|
|
60
|
+
this.wrapCache = { width: textWidth, lines };
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
// ─── Sizing ──────────────────────────────────────────────────────────────
|
|
64
|
+
get textWidth() {
|
|
65
|
+
const natural = Math.max(naturalWidth(this.detailValue), naturalWidth(this.documentationValue), MIN_WIDTH - TEXT_X - RIGHT_PAD - BORDER_THICKNESS);
|
|
66
|
+
return Math.min(natural, this.maxWidth - TEXT_X - RIGHT_PAD - BORDER_THICKNESS);
|
|
67
|
+
}
|
|
68
|
+
get boxWidth() {
|
|
69
|
+
if (this.isEmpty)
|
|
70
|
+
return 0;
|
|
71
|
+
return TEXT_X + this.textWidth + RIGHT_PAD + BORDER_THICKNESS;
|
|
72
|
+
}
|
|
73
|
+
get boxHeight() {
|
|
74
|
+
if (this.isEmpty)
|
|
75
|
+
return 0;
|
|
76
|
+
const lines = this.linesFor(this.textWidth).length;
|
|
77
|
+
return Math.min(this.maxHeight, lines + BORDER_THICKNESS * 2);
|
|
78
|
+
}
|
|
79
|
+
getMinIntrinsicWidth(_height) {
|
|
80
|
+
return this.boxWidth;
|
|
81
|
+
}
|
|
82
|
+
getMaxIntrinsicWidth(_height) {
|
|
83
|
+
return this.boxWidth;
|
|
84
|
+
}
|
|
85
|
+
getMinIntrinsicHeight(_width) {
|
|
86
|
+
return this.boxHeight;
|
|
87
|
+
}
|
|
88
|
+
getMaxIntrinsicHeight(_width) {
|
|
89
|
+
return this.boxHeight;
|
|
90
|
+
}
|
|
91
|
+
performLayout(constraints) {
|
|
92
|
+
const size = constraints.constrain(new Size(this.boxWidth, this.boxHeight));
|
|
93
|
+
super.performLayout(BoxConstraints.tight(size));
|
|
94
|
+
return size;
|
|
95
|
+
}
|
|
96
|
+
// ─── Render ──────────────────────────────────────────────────────────────
|
|
97
|
+
render(context) {
|
|
98
|
+
if (this.isEmpty)
|
|
99
|
+
return;
|
|
100
|
+
const w = this.layoutSize.width;
|
|
101
|
+
const h = this.layoutSize.height;
|
|
102
|
+
if (w <= 0 || h <= 0)
|
|
103
|
+
return;
|
|
104
|
+
context.drawBox(0, 0, w, h, {
|
|
105
|
+
fg: this.styleVar("editorSuggestWidget.border"),
|
|
106
|
+
bg: this.styleVar("editorSuggestWidget.background"),
|
|
107
|
+
fill: true,
|
|
108
|
+
});
|
|
109
|
+
const textWidth = Math.max(0, w - TEXT_X - RIGHT_PAD - BORDER_THICKNESS);
|
|
110
|
+
const lines = this.linesFor(textWidth);
|
|
111
|
+
const visible = Math.min(lines.length, Math.max(0, h - BORDER_THICKNESS * 2));
|
|
112
|
+
const detailLines = this.detailValue === "" ? 0 : wrapText(this.detailValue, textWidth).length;
|
|
113
|
+
for (let i = 0; i < visible; i++) {
|
|
114
|
+
// Сигнатура сверху — приглушённым цветом, как в правой колонке списка.
|
|
115
|
+
const isDetail = i < detailLines;
|
|
116
|
+
context.drawText(TEXT_X, BORDER_THICKNESS + i, lines[i], {
|
|
117
|
+
fg: this.styleVar(isDetail ? "editorSuggestWidget.detailForeground" : "editorSuggestWidget.foreground"),
|
|
118
|
+
bg: this.styleVar("editorSuggestWidget.background"),
|
|
119
|
+
}, { maxWidth: textWidth });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Ширина самой длинной строки текста как есть (до переноса). */
|
|
124
|
+
function naturalWidth(text) {
|
|
125
|
+
let max = 0;
|
|
126
|
+
for (const line of text.split("\n"))
|
|
127
|
+
max = Math.max(max, new DisplayLine(line).displayWidth);
|
|
128
|
+
return max;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Перенос по словам в ширину `width`. Переводы строк источника сохраняются;
|
|
132
|
+
* слово длиннее строки режется по ширине (усечения контента не происходит —
|
|
133
|
+
* остаток уезжает на следующую строку).
|
|
134
|
+
*/
|
|
135
|
+
export function wrapText(text, width) {
|
|
136
|
+
if (width <= 0)
|
|
137
|
+
return [];
|
|
138
|
+
const result = [];
|
|
139
|
+
for (const paragraph of text.split("\n")) {
|
|
140
|
+
let current = "";
|
|
141
|
+
for (const word of paragraph.split(/\s+/).filter((w) => w !== "")) {
|
|
142
|
+
const candidate = current === "" ? word : `${current} ${word}`;
|
|
143
|
+
if (new DisplayLine(candidate).displayWidth <= width) {
|
|
144
|
+
current = candidate;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (current !== "")
|
|
148
|
+
result.push(current);
|
|
149
|
+
// Слово шире строки: режем его по ширине, пока помещается остаток.
|
|
150
|
+
let rest = word;
|
|
151
|
+
while (new DisplayLine(rest).displayWidth > width) {
|
|
152
|
+
const head = truncateEnd(rest, width, "");
|
|
153
|
+
// Символ шире строки (широкий CJK при width 1) — усечение даёт
|
|
154
|
+
// пустую строку; режем принудительно, иначе цикл вечный.
|
|
155
|
+
if (head === "")
|
|
156
|
+
break;
|
|
157
|
+
result.push(head);
|
|
158
|
+
rest = rest.slice(head.length);
|
|
159
|
+
}
|
|
160
|
+
current = rest;
|
|
161
|
+
}
|
|
162
|
+
result.push(current);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Иконки для `CompletionItemKind` (числовые значения enum VS Code 0…26) —
|
|
3
|
+
* codicon-глифы (Private Use Area), как в nvim-cmp / lspkind preset "codicons".
|
|
4
|
+
* Требует nerd-font (со встроенными codicon'ами) в терминале.
|
|
5
|
+
*
|
|
6
|
+
* TUIDom-локальный, без зависимостей от Editor/Extensions — принимает сырое
|
|
7
|
+
* число `kind`.
|
|
8
|
+
*/
|
|
9
|
+
const KIND_ICONS = {
|
|
10
|
+
0: "", // Text
|
|
11
|
+
1: "", // Method
|
|
12
|
+
2: "", // Function
|
|
13
|
+
3: "", // Constructor
|
|
14
|
+
4: "", // Field
|
|
15
|
+
5: "", // Variable
|
|
16
|
+
6: "", // Class
|
|
17
|
+
7: "", // Interface
|
|
18
|
+
8: "", // Module
|
|
19
|
+
9: "", // Property
|
|
20
|
+
10: "", // Unit
|
|
21
|
+
11: "", // Value
|
|
22
|
+
12: "", // Enum
|
|
23
|
+
13: "", // Keyword
|
|
24
|
+
14: "", // Snippet
|
|
25
|
+
15: "", // Color
|
|
26
|
+
16: "", // File
|
|
27
|
+
17: "", // Reference
|
|
28
|
+
18: "", // Folder
|
|
29
|
+
19: "", // EnumMember
|
|
30
|
+
20: "", // Constant
|
|
31
|
+
21: "", // Struct
|
|
32
|
+
22: "", // Event
|
|
33
|
+
23: "", // Operator
|
|
34
|
+
24: "", // TypeParameter
|
|
35
|
+
};
|
|
36
|
+
/** Иконка по умолчанию для неизвестного/отсутствующего kind. */
|
|
37
|
+
const DEFAULT_ICON = ""; // Text
|
|
38
|
+
/** Возвращает codicon-глиф для `CompletionItemKind` (fallback — дефолтный). */
|
|
39
|
+
export function kindIcon(kind) {
|
|
40
|
+
if (kind === undefined)
|
|
41
|
+
return DEFAULT_ICON;
|
|
42
|
+
return KIND_ICONS[kind] ?? DEFAULT_ICON;
|
|
43
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
+
/**
|
|
4
|
+
* Элемент списка автодополнения (payload `data` непрозрачен для TUIDom — там
|
|
5
|
+
* хранится core-item). `kind` — числовой `CompletionItemKind` для иконки.
|
|
6
|
+
*/
|
|
7
|
+
export interface CompletionListItem {
|
|
8
|
+
readonly label: string;
|
|
9
|
+
readonly detail?: string;
|
|
10
|
+
/** Сигнатура сразу за лейблом (`(a: string): void`), приглушённая. */
|
|
11
|
+
readonly labelDetail?: string;
|
|
12
|
+
readonly kind?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Текст, по которому пункт фильтруется, если он отличается от лейбла.
|
|
15
|
+
* Не косметика: tsserver отдаёт после точки dot-accessor-пункты
|
|
16
|
+
* (`label: "getTime"`, `filterText: ".getTime"`), и фильтрация по лейблу
|
|
17
|
+
* не нашла бы ни одного при префиксе `.`.
|
|
18
|
+
*/
|
|
19
|
+
readonly filterText?: string;
|
|
20
|
+
/** Ключ сортировки источника (у LSP — `sortText`); при равенстве порядок исходный. */
|
|
21
|
+
readonly sortText?: string;
|
|
22
|
+
readonly data?: unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Компактный дропдаун автодополнения в стиле NvChad: рамка (углы `╭╮╰╯` —
|
|
26
|
+
* единый стиль с остальными оверлеями), выбранный ряд подсвечивается фоном (без
|
|
27
|
+
* указателей), 1-ячейка паддинга от рамки, колонка codicon-иконки типа.
|
|
28
|
+
* Собственной строки ввода нет — фильтр внутренний (набор символов сужает
|
|
29
|
+
* список, не трогая буфер редактора).
|
|
30
|
+
*
|
|
31
|
+
* Как в VS Code suggest widget попап **не забирает фокус** (`focusable = false`):
|
|
32
|
+
* навигацией/принятием/скрытием управляет {@link import("../../../src/vs/workbench/contrib/suggest/browser/completionService.js").CompletionService} через
|
|
33
|
+
* публичные методы (клавиши приходят командами по `suggestWidgetVisible`), а
|
|
34
|
+
* фильтрация идёт от префикса под кареткой редактора. Мышью: наведение
|
|
35
|
+
* подсвечивает ряд, клик принимает пункт.
|
|
36
|
+
*/
|
|
37
|
+
export declare class CompletionListElement extends TUIElement {
|
|
38
|
+
maxVisibleItems: number;
|
|
39
|
+
preferredWidth: number;
|
|
40
|
+
onAccept: ((item: CompletionListItem) => void) | null;
|
|
41
|
+
/**
|
|
42
|
+
* Сменился выбранный пункт (клавиатурой, мышью или пере-фильтрацией).
|
|
43
|
+
* По нему владелец догружает описание для панели — у LSP-источников оно
|
|
44
|
+
* приходит отдельным запросом на конкретный пункт.
|
|
45
|
+
*/
|
|
46
|
+
onSelectionChanged: ((item: CompletionListItem | null) => void) | null;
|
|
47
|
+
private allItems;
|
|
48
|
+
private filteredItems;
|
|
49
|
+
private filterValue;
|
|
50
|
+
private selectedIndexValue;
|
|
51
|
+
private scrollOffset;
|
|
52
|
+
/** Кэш {@link contentWidth}; сбрасывается сменой отфильтрованного списка. */
|
|
53
|
+
private contentWidthCache;
|
|
54
|
+
constructor();
|
|
55
|
+
/** Задаёт полный набор элементов; переприменяет текущий фильтр (fresh). */
|
|
56
|
+
setItems(items: readonly CompletionListItem[]): void;
|
|
57
|
+
/** Задаёт фильтр «начисто» (пустой результат сворачивает список). */
|
|
58
|
+
setFilter(value: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Инкрементальное сужение по мере набора: если новый префикс не совпал ни с
|
|
61
|
+
* чем — **оставляем последний непустой список** (как в VS Code), а не
|
|
62
|
+
* сворачиваем попап.
|
|
63
|
+
*/
|
|
64
|
+
refineFilter(value: string): void;
|
|
65
|
+
/** Видимые (отфильтрованные) элементы. */
|
|
66
|
+
get items(): readonly CompletionListItem[];
|
|
67
|
+
get selectedIndex(): number;
|
|
68
|
+
getSelectedItem(): CompletionListItem | null;
|
|
69
|
+
private applyFilter;
|
|
70
|
+
private get visibleItemCount();
|
|
71
|
+
/**
|
|
72
|
+
* Ширина содержимого. Считается один раз на смену списка и кэшируется:
|
|
73
|
+
* layout зовут на каждый кадр, а список от language server'а — это сотни
|
|
74
|
+
* и тысячи пунктов. По той же причине меряем только первые
|
|
75
|
+
* {@link WIDTH_SAMPLE_LIMIT} — попап всё равно упирается в `preferredWidth`.
|
|
76
|
+
*/
|
|
77
|
+
private get contentWidth();
|
|
78
|
+
private get boxWidth();
|
|
79
|
+
private get boxHeight();
|
|
80
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
81
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
82
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
83
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
84
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
85
|
+
render(context: RenderContext): void;
|
|
86
|
+
private renderRow;
|
|
87
|
+
selectNext(): void;
|
|
88
|
+
selectPrevious(): void;
|
|
89
|
+
selectNextPage(): void;
|
|
90
|
+
selectPreviousPage(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Индекс пункта под локальной Y, или `null`, если это рамка/за пределами
|
|
93
|
+
* видимых строк. `visibleItemCount` + `scrollOffset` гарантируют, что
|
|
94
|
+
* возвращённый индекс всегда в пределах `filteredItems`.
|
|
95
|
+
*/
|
|
96
|
+
private rowAt;
|
|
97
|
+
private handleMouseMove;
|
|
98
|
+
private handleClick;
|
|
99
|
+
private moveSelection;
|
|
100
|
+
private fireSelectionChanged;
|
|
101
|
+
private ensureVisible;
|
|
102
|
+
}
|