@singapore-editor/plugin-ui 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/README.md +27 -0
- package/dist/anchoredSurface.d.ts +48 -0
- package/dist/anchoredSurface.d.ts.map +1 -0
- package/dist/anchoredSurface.js +140 -0
- package/dist/anchoredSurface.js.map +1 -0
- package/dist/codeTokens.d.ts +24 -0
- package/dist/codeTokens.d.ts.map +1 -0
- package/dist/codeTokens.js +78 -0
- package/dist/codeTokens.js.map +1 -0
- package/dist/hoverController.d.ts +30 -0
- package/dist/hoverController.d.ts.map +1 -0
- package/dist/hoverController.js +277 -0
- package/dist/hoverController.js.map +1 -0
- package/dist/hoverParticipant.d.ts +48 -0
- package/dist/hoverParticipant.d.ts.map +1 -0
- package/dist/hoverParticipant.js +2 -0
- package/dist/hoverPlugin.d.ts +14 -0
- package/dist/hoverPlugin.d.ts.map +1 -0
- package/dist/hoverPlugin.js +49 -0
- package/dist/hoverPlugin.js.map +1 -0
- package/dist/hoverRegistry.d.ts +7 -0
- package/dist/hoverRegistry.d.ts.map +1 -0
- package/dist/hoverRegistry.js +24 -0
- package/dist/hoverRegistry.js.map +1 -0
- package/dist/hoverToken.d.ts +9 -0
- package/dist/hoverToken.d.ts.map +1 -0
- package/dist/hoverToken.js +17 -0
- package/dist/hoverToken.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/markdownTooltip.d.ts +12 -0
- package/dist/markdownTooltip.d.ts.map +1 -0
- package/dist/markdownTooltip.js +282 -0
- package/dist/markdownTooltip.js.map +1 -0
- package/dist/offsetRange.d.ts +24 -0
- package/dist/offsetRange.d.ts.map +1 -0
- package/dist/offsetRange.js +81 -0
- package/dist/offsetRange.js.map +1 -0
- package/dist/styles.d.ts +13 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +39 -0
- package/dist/styles.js.map +1 -0
- package/dist/tooltip.d.ts +71 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +786 -0
- package/dist/tooltip.js.map +1 -0
- package/package.json +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @singapore-editor/plugin-ui
|
|
2
|
+
|
|
3
|
+
The floating surfaces editor plugins share: an anchored popup, a hover tooltip, and the Markdown
|
|
4
|
+
renderer the tooltip paints with. Language servers, diff hovers and character warnings all show the
|
|
5
|
+
same tooltip, so it lives here rather than in any one of them.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createTooltipController, HOVER_REQUEST_DEBOUNCE_MS } from '@singapore-editor/plugin-ui'
|
|
9
|
+
|
|
10
|
+
const tooltip = createTooltipController({
|
|
11
|
+
document,
|
|
12
|
+
themeSource: editorElement,
|
|
13
|
+
reentryElement: editorElement,
|
|
14
|
+
classNamespace: 'my-plugin',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
tooltip.show({
|
|
18
|
+
anchor: rect,
|
|
19
|
+
hoverText: '**Ambiguous character** U+2013',
|
|
20
|
+
notes: [{ label: 'warning', color: 'var(--editor-warning)', text: 'Looks like a hyphen' }],
|
|
21
|
+
actions: [{ label: 'Adjust settings', run: openSettings }],
|
|
22
|
+
theme,
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Every surface placed by `createAnchoredSurface` carries `data-editor-popup`, so a host styles all of
|
|
27
|
+
them with one selector instead of one class per plugin.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EditorDisposable, EditorViewContributionUpdateKind } from '@singapore-editor/core/extensions';
|
|
2
|
+
/** Whether an event target sits inside any floating editor surface. */
|
|
3
|
+
export declare function isInsideEditorPopup(target: EventTarget | null): boolean;
|
|
4
|
+
/** Which side of the anchor the surface takes. */
|
|
5
|
+
export type AnchoredSurfacePlacement = 'top' | 'bottom';
|
|
6
|
+
/**
|
|
7
|
+
* How the surface lines up across the anchor: centred over it, or starting at its leading edge and
|
|
8
|
+
* running away from it, the way a list read left to right wants to.
|
|
9
|
+
*/
|
|
10
|
+
export type AnchoredSurfaceAlignment = 'center' | 'start';
|
|
11
|
+
export type AnchoredSurfaceOptions = {
|
|
12
|
+
/** The floating element. Its owner styles and mounts it; only where it lands is ours. */
|
|
13
|
+
readonly element: HTMLElement;
|
|
14
|
+
/** Class for the anchor element, and the stem of the anchor name the surface follows. */
|
|
15
|
+
readonly anchorClassName: string;
|
|
16
|
+
readonly preferredPlacement: AnchoredSurfacePlacement;
|
|
17
|
+
readonly alignment?: AnchoredSurfaceAlignment;
|
|
18
|
+
/** Clear space kept between the anchor and the surface. */
|
|
19
|
+
readonly gapPx?: number;
|
|
20
|
+
/** Clear space kept between the surface and the edge of the viewport. */
|
|
21
|
+
readonly viewportMarginPx?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Tallest the surface may grow on either side. A surface with nothing to scroll leaves it out: a
|
|
24
|
+
* ceiling on a single-line input is a number no reader could ever act on.
|
|
25
|
+
*/
|
|
26
|
+
readonly maxHeightPx?: number | (() => number);
|
|
27
|
+
/** The ceiling the surface landed with, for a surface whose scrolling body needs it too. */
|
|
28
|
+
onPlaced?(maxHeightPx: number, placement: AnchoredSurfacePlacement): void;
|
|
29
|
+
};
|
|
30
|
+
export type AnchoredSurface = EditorDisposable & {
|
|
31
|
+
/**
|
|
32
|
+
* Puts the surface beside `anchor`, on the side it fits and with the height that side has room
|
|
33
|
+
* for. Render the content first — the side is chosen from how tall the surface actually is.
|
|
34
|
+
*/
|
|
35
|
+
place(anchor: DOMRect, preferredPlacement?: AnchoredSurfacePlacement): void;
|
|
36
|
+
/** Takes the anchor out of the page while the surface is off screen. */
|
|
37
|
+
release(): void;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Whether an update moved the text under a surface without changing what the surface says.
|
|
41
|
+
*
|
|
42
|
+
* A surface is placed against a rect in viewport coordinates, so a scroll or a relayout leaves it
|
|
43
|
+
* pointing at text that has moved out from under it. Which kinds those are is one question with one
|
|
44
|
+
* answer, asked from four controllers.
|
|
45
|
+
*/
|
|
46
|
+
export declare function anchoredSurfaceFollowsUpdate(kind: EditorViewContributionUpdateKind): boolean;
|
|
47
|
+
export declare function createAnchoredSurface(options: AnchoredSurfaceOptions): AnchoredSurface;
|
|
48
|
+
//# sourceMappingURL=anchoredSurface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchoredSurface.d.ts","sourceRoot":"","sources":["../src/anchoredSurface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,gCAAgC,EACjC,MAAM,mCAAmC,CAAA;AAgB1C,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAGvE;AAED,kDAAkD;AAClD,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,QAAQ,CAAA;AAEvD;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,OAAO,CAAA;AAazD,MAAM,MAAM,sBAAsB,GAAG;IACnC,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,kBAAkB,EAAE,wBAAwB,CAAA;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,wBAAwB,CAAA;IAC7C,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;IAC9C,4FAA4F;IAC5F,QAAQ,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,wBAAwB,GAAG,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG;IAC/C;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,kBAAkB,CAAC,EAAE,wBAAwB,GAAG,IAAI,CAAA;IAC3E,wEAAwE;IACxE,OAAO,IAAI,IAAI,CAAA;CAChB,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,gCAAgC,GAAG,OAAO,CAE5F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAoEtF"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//#region src/anchoredSurface.ts
|
|
2
|
+
/**
|
|
3
|
+
* The placement every floating surface a plugin puts up shares.
|
|
4
|
+
*
|
|
5
|
+
* Anchor positioning does the page arithmetic: a hidden anchor element carries `anchor-name`, the
|
|
6
|
+
* surface carries `position-anchor`, and `position-area` drops it on the side we name — nothing
|
|
7
|
+
* here measures the viewport to write a left and a top. What anchor positioning will not decide is
|
|
8
|
+
* which side to ask for when the preferred one has no room, and how tall the surface may grow once
|
|
9
|
+
* a side is chosen. That decision reads the same for a hover, a completion list, a signature and a
|
|
10
|
+
* rename input, and it used to be written out once per surface.
|
|
11
|
+
*/
|
|
12
|
+
/** One hook for a host to style every floating editor surface, whatever plugin owns it. */
|
|
13
|
+
var EDITOR_POPUP_ATTRIBUTE = "data-editor-popup";
|
|
14
|
+
/** Whether an event target sits inside any floating editor surface. */
|
|
15
|
+
function isInsideEditorPopup(target) {
|
|
16
|
+
if (!(target instanceof Element)) return false;
|
|
17
|
+
return target.closest(`[${EDITOR_POPUP_ATTRIBUTE}]`) !== null;
|
|
18
|
+
}
|
|
19
|
+
var DEFAULT_GAP_PX = 8;
|
|
20
|
+
var DEFAULT_VIEWPORT_MARGIN_PX = 12;
|
|
21
|
+
/**
|
|
22
|
+
* Shortest ceiling worth writing. A surface anchored hard against the edge of the viewport would
|
|
23
|
+
* otherwise be clamped down to nothing; overflowing the margin by a little is the better failure.
|
|
24
|
+
*/
|
|
25
|
+
var MIN_SURFACE_HEIGHT_PX = 80;
|
|
26
|
+
var nextAnchorId = 0;
|
|
27
|
+
/**
|
|
28
|
+
* Whether an update moved the text under a surface without changing what the surface says.
|
|
29
|
+
*
|
|
30
|
+
* A surface is placed against a rect in viewport coordinates, so a scroll or a relayout leaves it
|
|
31
|
+
* pointing at text that has moved out from under it. Which kinds those are is one question with one
|
|
32
|
+
* answer, asked from four controllers.
|
|
33
|
+
*/
|
|
34
|
+
function anchoredSurfaceFollowsUpdate(kind) {
|
|
35
|
+
return kind === "viewport" || kind === "layout";
|
|
36
|
+
}
|
|
37
|
+
function createAnchoredSurface(options) {
|
|
38
|
+
const { element } = options;
|
|
39
|
+
const document = element.ownerDocument;
|
|
40
|
+
const gapPx = options.gapPx ?? DEFAULT_GAP_PX;
|
|
41
|
+
const viewportMarginPx = options.viewportMarginPx ?? DEFAULT_VIEWPORT_MARGIN_PX;
|
|
42
|
+
const alignment = options.alignment ?? "center";
|
|
43
|
+
nextAnchorId += 1;
|
|
44
|
+
const anchorName = `--${options.anchorClassName}-${nextAnchorId}`;
|
|
45
|
+
const anchor = createAnchorElement(document, options.anchorClassName, anchorName);
|
|
46
|
+
if (element.parentNode) element.before(anchor);
|
|
47
|
+
else document.body.prepend(anchor);
|
|
48
|
+
element.setAttribute(EDITOR_POPUP_ATTRIBUTE, "");
|
|
49
|
+
element.style.position = "fixed";
|
|
50
|
+
element.style.setProperty("position-anchor", anchorName);
|
|
51
|
+
element.style.setProperty("inset", "auto");
|
|
52
|
+
if (alignment === "start") element.style.setProperty("position-try-fallbacks", "flip-inline");
|
|
53
|
+
applyPlacement(element, options.preferredPlacement, alignment, gapPx);
|
|
54
|
+
let disposed = false;
|
|
55
|
+
const place = (rect, preferredPlacement = options.preferredPlacement) => {
|
|
56
|
+
positionAnchorElement(anchor, rect);
|
|
57
|
+
const viewportHeight = document.defaultView?.innerHeight ?? 0;
|
|
58
|
+
const configuredMaxHeight = resolveMaxHeight(options.maxHeightPx);
|
|
59
|
+
const placement = fittingPlacement(rect, surfaceHeight(element, configuredMaxHeight), viewportHeight, preferredPlacement, gapPx, viewportMarginPx);
|
|
60
|
+
applyPlacement(element, placement, alignment, gapPx);
|
|
61
|
+
if (configuredMaxHeight === void 0) return;
|
|
62
|
+
const maxHeightPx = surfaceCeiling(roomBeside(rect, placement, viewportHeight, gapPx, viewportMarginPx), configuredMaxHeight);
|
|
63
|
+
element.style.maxHeight = `${maxHeightPx}px`;
|
|
64
|
+
options.onPlaced?.(maxHeightPx, placement);
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
place,
|
|
68
|
+
release: () => {
|
|
69
|
+
anchor.style.display = "none";
|
|
70
|
+
},
|
|
71
|
+
dispose: () => {
|
|
72
|
+
if (disposed) return;
|
|
73
|
+
disposed = true;
|
|
74
|
+
anchor.remove();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function resolveMaxHeight(maxHeightPx) {
|
|
79
|
+
if (typeof maxHeightPx === "function") return maxHeightPx();
|
|
80
|
+
return maxHeightPx;
|
|
81
|
+
}
|
|
82
|
+
function createAnchorElement(document, className, anchorName) {
|
|
83
|
+
const element = document.createElement("div");
|
|
84
|
+
element.className = className;
|
|
85
|
+
Object.assign(element.style, {
|
|
86
|
+
position: "fixed",
|
|
87
|
+
display: "none",
|
|
88
|
+
opacity: "0",
|
|
89
|
+
pointerEvents: "none"
|
|
90
|
+
});
|
|
91
|
+
element.style.setProperty("anchor-name", anchorName);
|
|
92
|
+
return element;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The anchor stands in for the text the surface belongs to, so it is given that text's rect and
|
|
96
|
+
* nothing else. A degenerate rect — a caret, an empty range — would leave nothing to anchor to, so
|
|
97
|
+
* it is widened to the smallest rect the browser will still lay out.
|
|
98
|
+
*/
|
|
99
|
+
function positionAnchorElement(element, rect) {
|
|
100
|
+
Object.assign(element.style, {
|
|
101
|
+
display: "block",
|
|
102
|
+
left: `${rect.left}px`,
|
|
103
|
+
top: `${rect.top}px`,
|
|
104
|
+
width: `${Math.max(1, rect.width)}px`,
|
|
105
|
+
height: `${Math.max(1, rect.height)}px`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function applyPlacement(element, placement, alignment, gapPx) {
|
|
109
|
+
const across = alignment === "start" ? "span-right" : "center";
|
|
110
|
+
element.style.setProperty("position-area", `${placement} ${across}`);
|
|
111
|
+
element.style.setProperty("margin-top", placement === "bottom" ? `${gapPx}px` : "0");
|
|
112
|
+
element.style.setProperty("margin-bottom", placement === "top" ? `${gapPx}px` : "0");
|
|
113
|
+
}
|
|
114
|
+
function surfaceHeight(element, maxHeightPx) {
|
|
115
|
+
const measured = element.getBoundingClientRect().height;
|
|
116
|
+
if (maxHeightPx === void 0) return measured;
|
|
117
|
+
return Math.min(measured, maxHeightPx);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The side the surface goes on: the one asked for while it fits, then the other, then whichever has
|
|
121
|
+
* more room for a surface that fits on neither.
|
|
122
|
+
*/
|
|
123
|
+
function fittingPlacement(anchor, height, viewportHeight, preferred, gapPx, viewportMarginPx) {
|
|
124
|
+
const room = (placement) => roomBeside(anchor, placement, viewportHeight, gapPx, viewportMarginPx);
|
|
125
|
+
if (room(preferred) >= height) return preferred;
|
|
126
|
+
const fallback = preferred === "top" ? "bottom" : "top";
|
|
127
|
+
if (room(fallback) >= height) return fallback;
|
|
128
|
+
return room("top") >= room("bottom") ? "top" : "bottom";
|
|
129
|
+
}
|
|
130
|
+
function roomBeside(anchor, placement, viewportHeight, gapPx, viewportMarginPx) {
|
|
131
|
+
if (placement === "top") return anchor.top - gapPx - viewportMarginPx;
|
|
132
|
+
return viewportHeight - anchor.bottom - gapPx - viewportMarginPx;
|
|
133
|
+
}
|
|
134
|
+
function surfaceCeiling(room, maxHeightPx) {
|
|
135
|
+
return Math.max(MIN_SURFACE_HEIGHT_PX, Math.min(maxHeightPx, Math.floor(room)));
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { anchoredSurfaceFollowsUpdate, createAnchoredSurface, isInsideEditorPopup };
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=anchoredSurface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchoredSurface.js","names":[],"sources":["../src/anchoredSurface.ts"],"sourcesContent":["import type {\n EditorDisposable,\n EditorViewContributionUpdateKind,\n} from '@singapore-editor/core/extensions'\n\n/**\n * The placement every floating surface a plugin puts up shares.\n *\n * Anchor positioning does the page arithmetic: a hidden anchor element carries `anchor-name`, the\n * surface carries `position-anchor`, and `position-area` drops it on the side we name — nothing\n * here measures the viewport to write a left and a top. What anchor positioning will not decide is\n * which side to ask for when the preferred one has no room, and how tall the surface may grow once\n * a side is chosen. That decision reads the same for a hover, a completion list, a signature and a\n * rename input, and it used to be written out once per surface.\n */\n\n/** One hook for a host to style every floating editor surface, whatever plugin owns it. */\nconst EDITOR_POPUP_ATTRIBUTE = 'data-editor-popup'\n\n/** Whether an event target sits inside any floating editor surface. */\nexport function isInsideEditorPopup(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false\n return target.closest(`[${EDITOR_POPUP_ATTRIBUTE}]`) !== null\n}\n\n/** Which side of the anchor the surface takes. */\nexport type AnchoredSurfacePlacement = 'top' | 'bottom'\n\n/**\n * How the surface lines up across the anchor: centred over it, or starting at its leading edge and\n * running away from it, the way a list read left to right wants to.\n */\nexport type AnchoredSurfaceAlignment = 'center' | 'start'\n\nconst DEFAULT_GAP_PX = 8\nconst DEFAULT_VIEWPORT_MARGIN_PX = 12\n\n/**\n * Shortest ceiling worth writing. A surface anchored hard against the edge of the viewport would\n * otherwise be clamped down to nothing; overflowing the margin by a little is the better failure.\n */\nconst MIN_SURFACE_HEIGHT_PX = 80\n\nlet nextAnchorId = 0\n\nexport type AnchoredSurfaceOptions = {\n /** The floating element. Its owner styles and mounts it; only where it lands is ours. */\n readonly element: HTMLElement\n /** Class for the anchor element, and the stem of the anchor name the surface follows. */\n readonly anchorClassName: string\n readonly preferredPlacement: AnchoredSurfacePlacement\n readonly alignment?: AnchoredSurfaceAlignment\n /** Clear space kept between the anchor and the surface. */\n readonly gapPx?: number\n /** Clear space kept between the surface and the edge of the viewport. */\n readonly viewportMarginPx?: number\n /**\n * Tallest the surface may grow on either side. A surface with nothing to scroll leaves it out: a\n * ceiling on a single-line input is a number no reader could ever act on.\n */\n readonly maxHeightPx?: number | (() => number)\n /** The ceiling the surface landed with, for a surface whose scrolling body needs it too. */\n onPlaced?(maxHeightPx: number, placement: AnchoredSurfacePlacement): void\n}\n\nexport type AnchoredSurface = EditorDisposable & {\n /**\n * Puts the surface beside `anchor`, on the side it fits and with the height that side has room\n * for. Render the content first — the side is chosen from how tall the surface actually is.\n */\n place(anchor: DOMRect, preferredPlacement?: AnchoredSurfacePlacement): void\n /** Takes the anchor out of the page while the surface is off screen. */\n release(): void\n}\n\n/**\n * Whether an update moved the text under a surface without changing what the surface says.\n *\n * A surface is placed against a rect in viewport coordinates, so a scroll or a relayout leaves it\n * pointing at text that has moved out from under it. Which kinds those are is one question with one\n * answer, asked from four controllers.\n */\nexport function anchoredSurfaceFollowsUpdate(kind: EditorViewContributionUpdateKind): boolean {\n return kind === 'viewport' || kind === 'layout'\n}\n\nexport function createAnchoredSurface(options: AnchoredSurfaceOptions): AnchoredSurface {\n const { element } = options\n const document = element.ownerDocument\n const gapPx = options.gapPx ?? DEFAULT_GAP_PX\n const viewportMarginPx = options.viewportMarginPx ?? DEFAULT_VIEWPORT_MARGIN_PX\n const alignment = options.alignment ?? 'center'\n\n nextAnchorId += 1\n const anchorName = `--${options.anchorClassName}-${nextAnchorId}`\n const anchor = createAnchorElement(document, options.anchorClassName, anchorName)\n // Anchor positioning only accepts an anchor that precedes the positioned element in tree order.\n // Every owner mounts its surface before asking for one, so appending here would put the anchor\n // after it and leave the surface with no anchor at all — silently placed at its static position,\n // in the bottom corner of the page, with the right content and the wrong coordinates.\n if (element.parentNode) element.before(anchor)\n else document.body.prepend(anchor)\n\n // One hook for a host to style every floating editor surface, whatever plugin owns it.\n element.setAttribute(EDITOR_POPUP_ATTRIBUTE, '')\n element.style.position = 'fixed'\n element.style.setProperty('position-anchor', anchorName)\n // The surface is placed entirely by its anchor, so any inset left over from an earlier life of\n // the element would fight the position area rather than add to it.\n element.style.setProperty('inset', 'auto')\n // A surface that starts at the anchor runs off the far edge of a narrow window; the side is ours\n // and stays ours, so only the axis we did not decide is handed back to the browser.\n if (alignment === 'start') element.style.setProperty('position-try-fallbacks', 'flip-inline')\n applyPlacement(element, options.preferredPlacement, alignment, gapPx)\n\n let disposed = false\n\n const place = (\n rect: DOMRect,\n preferredPlacement: AnchoredSurfacePlacement = options.preferredPlacement,\n ): void => {\n positionAnchorElement(anchor, rect)\n const viewportHeight = document.defaultView?.innerHeight ?? 0\n const configuredMaxHeight = resolveMaxHeight(options.maxHeightPx)\n const height = surfaceHeight(element, configuredMaxHeight)\n const placement = fittingPlacement(\n rect,\n height,\n viewportHeight,\n preferredPlacement,\n gapPx,\n viewportMarginPx,\n )\n applyPlacement(element, placement, alignment, gapPx)\n if (configuredMaxHeight === undefined) return\n\n const room = roomBeside(rect, placement, viewportHeight, gapPx, viewportMarginPx)\n const maxHeightPx = surfaceCeiling(room, configuredMaxHeight)\n element.style.maxHeight = `${maxHeightPx}px`\n options.onPlaced?.(maxHeightPx, placement)\n }\n\n return {\n place,\n release: () => {\n anchor.style.display = 'none'\n },\n dispose: () => {\n if (disposed) return\n\n disposed = true\n anchor.remove()\n },\n }\n}\n\nfunction resolveMaxHeight(maxHeightPx: number | (() => number) | undefined): number | undefined {\n if (typeof maxHeightPx === 'function') return maxHeightPx()\n return maxHeightPx\n}\n\nfunction createAnchorElement(\n document: Document,\n className: string,\n anchorName: string,\n): HTMLDivElement {\n const element = document.createElement('div')\n element.className = className\n Object.assign(element.style, {\n position: 'fixed',\n display: 'none',\n opacity: '0',\n pointerEvents: 'none',\n })\n element.style.setProperty('anchor-name', anchorName)\n return element\n}\n\n/**\n * The anchor stands in for the text the surface belongs to, so it is given that text's rect and\n * nothing else. A degenerate rect — a caret, an empty range — would leave nothing to anchor to, so\n * it is widened to the smallest rect the browser will still lay out.\n */\nfunction positionAnchorElement(element: HTMLDivElement, rect: DOMRect): void {\n Object.assign(element.style, {\n display: 'block',\n left: `${rect.left}px`,\n top: `${rect.top}px`,\n width: `${Math.max(1, rect.width)}px`,\n height: `${Math.max(1, rect.height)}px`,\n })\n}\n\nfunction applyPlacement(\n element: HTMLElement,\n placement: AnchoredSurfacePlacement,\n alignment: AnchoredSurfaceAlignment,\n gapPx: number,\n): void {\n const across = alignment === 'start' ? 'span-right' : 'center'\n element.style.setProperty('position-area', `${placement} ${across}`)\n // The gap is a margin rather than an offset because the position area owns the insets, and a\n // margin is the one thing that still pushes a surface away from its anchor from inside it.\n element.style.setProperty('margin-top', placement === 'bottom' ? `${gapPx}px` : '0')\n element.style.setProperty('margin-bottom', placement === 'top' ? `${gapPx}px` : '0')\n}\n\nfunction surfaceHeight(element: HTMLElement, maxHeightPx: number | undefined): number {\n const measured = element.getBoundingClientRect().height\n if (maxHeightPx === undefined) return measured\n\n // What the surface would settle at, not what it is showing: a list already clamped to its ceiling\n // must not read as too tall for a side that would hold it at that ceiling.\n return Math.min(measured, maxHeightPx)\n}\n\n/**\n * The side the surface goes on: the one asked for while it fits, then the other, then whichever has\n * more room for a surface that fits on neither.\n */\nfunction fittingPlacement(\n anchor: DOMRect,\n height: number,\n viewportHeight: number,\n preferred: AnchoredSurfacePlacement,\n gapPx: number,\n viewportMarginPx: number,\n): AnchoredSurfacePlacement {\n const room = (placement: AnchoredSurfacePlacement): number =>\n roomBeside(anchor, placement, viewportHeight, gapPx, viewportMarginPx)\n if (room(preferred) >= height) return preferred\n\n const fallback = preferred === 'top' ? 'bottom' : 'top'\n if (room(fallback) >= height) return fallback\n\n return room('top') >= room('bottom') ? 'top' : 'bottom'\n}\n\nfunction roomBeside(\n anchor: DOMRect,\n placement: AnchoredSurfacePlacement,\n viewportHeight: number,\n gapPx: number,\n viewportMarginPx: number,\n): number {\n if (placement === 'top') return anchor.top - gapPx - viewportMarginPx\n return viewportHeight - anchor.bottom - gapPx - viewportMarginPx\n}\n\nfunction surfaceCeiling(room: number, maxHeightPx: number): number {\n return Math.max(MIN_SURFACE_HEIGHT_PX, Math.min(maxHeightPx, Math.floor(room)))\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,IAAM,yBAAyB;;AAG/B,SAAgB,oBAAoB,QAAqC;CACvE,IAAI,EAAE,kBAAkB,UAAU,OAAO;CACzC,OAAO,OAAO,QAAQ,IAAI,uBAAuB,EAAE,MAAM;AAC3D;AAWA,IAAM,iBAAiB;AACvB,IAAM,6BAA6B;;;;;AAMnC,IAAM,wBAAwB;AAE9B,IAAI,eAAe;;;;;;;;AAuCnB,SAAgB,6BAA6B,MAAiD;CAC5F,OAAO,SAAS,cAAc,SAAS;AACzC;AAEA,SAAgB,sBAAsB,SAAkD;CACtF,MAAM,EAAE,YAAY;CACpB,MAAM,WAAW,QAAQ;CACzB,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,mBAAmB,QAAQ,oBAAoB;CACrD,MAAM,YAAY,QAAQ,aAAa;CAEvC,gBAAgB;CAChB,MAAM,aAAa,KAAK,QAAQ,gBAAgB,GAAG;CACnD,MAAM,SAAS,oBAAoB,UAAU,QAAQ,iBAAiB,UAAU;CAKhF,IAAI,QAAQ,YAAY,QAAQ,OAAO,MAAM;MACxC,SAAS,KAAK,QAAQ,MAAM;CAGjC,QAAQ,aAAa,wBAAwB,EAAE;CAC/C,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,YAAY,mBAAmB,UAAU;CAGvD,QAAQ,MAAM,YAAY,SAAS,MAAM;CAGzC,IAAI,cAAc,SAAS,QAAQ,MAAM,YAAY,0BAA0B,aAAa;CAC5F,eAAe,SAAS,QAAQ,oBAAoB,WAAW,KAAK;CAEpE,IAAI,WAAW;CAEf,MAAM,SACJ,MACA,qBAA+C,QAAQ,uBAC9C;EACT,sBAAsB,QAAQ,IAAI;EAClC,MAAM,iBAAiB,SAAS,aAAa,eAAe;EAC5D,MAAM,sBAAsB,iBAAiB,QAAQ,WAAW;EAEhE,MAAM,YAAY,iBAChB,MAFa,cAAc,SAAS,mBAGpC,GACA,gBACA,oBACA,OACA,gBACF;EACA,eAAe,SAAS,WAAW,WAAW,KAAK;EACnD,IAAI,wBAAwB,KAAA,GAAW;EAGvC,MAAM,cAAc,eADP,WAAW,MAAM,WAAW,gBAAgB,OAAO,gBAC7B,GAAM,mBAAmB;EAC5D,QAAQ,MAAM,YAAY,GAAG,YAAY;EACzC,QAAQ,WAAW,aAAa,SAAS;CAC3C;CAEA,OAAO;EACL;EACA,eAAe;GACb,OAAO,MAAM,UAAU;EACzB;EACA,eAAe;GACb,IAAI,UAAU;GAEd,WAAW;GACX,OAAO,OAAO;EAChB;CACF;AACF;AAEA,SAAS,iBAAiB,aAAsE;CAC9F,IAAI,OAAO,gBAAgB,YAAY,OAAO,YAAY;CAC1D,OAAO;AACT;AAEA,SAAS,oBACP,UACA,WACA,YACgB;CAChB,MAAM,UAAU,SAAS,cAAc,KAAK;CAC5C,QAAQ,YAAY;CACpB,OAAO,OAAO,QAAQ,OAAO;EAC3B,UAAU;EACV,SAAS;EACT,SAAS;EACT,eAAe;CACjB,CAAC;CACD,QAAQ,MAAM,YAAY,eAAe,UAAU;CACnD,OAAO;AACT;;;;;;AAOA,SAAS,sBAAsB,SAAyB,MAAqB;CAC3E,OAAO,OAAO,QAAQ,OAAO;EAC3B,SAAS;EACT,MAAM,GAAG,KAAK,KAAK;EACnB,KAAK,GAAG,KAAK,IAAI;EACjB,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,KAAK,EAAE;EAClC,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE;CACtC,CAAC;AACH;AAEA,SAAS,eACP,SACA,WACA,WACA,OACM;CACN,MAAM,SAAS,cAAc,UAAU,eAAe;CACtD,QAAQ,MAAM,YAAY,iBAAiB,GAAG,UAAU,GAAG,QAAQ;CAGnE,QAAQ,MAAM,YAAY,cAAc,cAAc,WAAW,GAAG,MAAM,MAAM,GAAG;CACnF,QAAQ,MAAM,YAAY,iBAAiB,cAAc,QAAQ,GAAG,MAAM,MAAM,GAAG;AACrF;AAEA,SAAS,cAAc,SAAsB,aAAyC;CACpF,MAAM,WAAW,QAAQ,sBAAsB,CAAC,CAAC;CACjD,IAAI,gBAAgB,KAAA,GAAW,OAAO;CAItC,OAAO,KAAK,IAAI,UAAU,WAAW;AACvC;;;;;AAMA,SAAS,iBACP,QACA,QACA,gBACA,WACA,OACA,kBAC0B;CAC1B,MAAM,QAAQ,cACZ,WAAW,QAAQ,WAAW,gBAAgB,OAAO,gBAAgB;CACvE,IAAI,KAAK,SAAS,KAAK,QAAQ,OAAO;CAEtC,MAAM,WAAW,cAAc,QAAQ,WAAW;CAClD,IAAI,KAAK,QAAQ,KAAK,QAAQ,OAAO;CAErC,OAAO,KAAK,KAAK,KAAK,KAAK,QAAQ,IAAI,QAAQ;AACjD;AAEA,SAAS,WACP,QACA,WACA,gBACA,OACA,kBACQ;CACR,IAAI,cAAc,OAAO,OAAO,OAAO,MAAM,QAAQ;CACrD,OAAO,iBAAiB,OAAO,SAAS,QAAQ;AAClD;AAEA,SAAS,eAAe,MAAc,aAA6B;CACjE,OAAO,KAAK,IAAI,uBAAuB,KAAK,IAAI,aAAa,KAAK,MAAM,IAAI,CAAC,CAAC;AAChF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EditorSnippetTokensFeature } from '@singapore-editor/core/extensions';
|
|
2
|
+
import type { EditorToken } from '@singapore-editor/core/syntax';
|
|
3
|
+
export type TooltipCodeBlock = {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly languageId: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The editor's own tokens for a hover's fenced code. Settled answers are kept because a hover
|
|
9
|
+
* re-renders whole as each participant reports, and a block that repainted from plain on every
|
|
10
|
+
* render would flash. Tokens can carry a theme's literal colours, so a theme change clears them.
|
|
11
|
+
*/
|
|
12
|
+
export type TooltipCodeTokenizer = {
|
|
13
|
+
cached(text: string, languageId: string): readonly EditorToken[] | null;
|
|
14
|
+
tokenize(text: string, languageId: string): Promise<readonly EditorToken[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Null when every block can paint in colour now, or was already waited for once. Otherwise
|
|
17
|
+
* settles when they can or the hold runs out, so a hover opens coloured instead of turning.
|
|
18
|
+
*/
|
|
19
|
+
prepare(blocks: readonly TooltipCodeBlock[]): Promise<void> | null;
|
|
20
|
+
clear(): void;
|
|
21
|
+
};
|
|
22
|
+
export declare function createTooltipCodeTokenizer(feature: Pick<EditorSnippetTokensFeature, 'tokenize'>): TooltipCodeTokenizer;
|
|
23
|
+
export declare function paintCodeTokens(document: Document, code: HTMLElement, text: string, tokens: readonly EditorToken[]): void;
|
|
24
|
+
//# sourceMappingURL=codeTokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeTokens.d.ts","sourceRoot":"","sources":["../src/codeTokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAA;AACnF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAA;AAQhE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,WAAW,EAAE,GAAG,IAAI,CAAA;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAA;IAC3E;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAClE,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,IAAI,CAAC,0BAA0B,EAAE,UAAU,CAAC,GACpD,oBAAoB,CAqDtB;AAQD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,WAAW,EAAE,GAC7B,IAAI,CAgBN"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/codeTokens.ts
|
|
2
|
+
var MAX_CACHED_SNIPPETS = 64;
|
|
3
|
+
var TOKEN_HOLD_MS = 250;
|
|
4
|
+
function createTooltipCodeTokenizer(feature) {
|
|
5
|
+
const settled = /* @__PURE__ */ new Map();
|
|
6
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
7
|
+
const heldOnce = /* @__PURE__ */ new Set();
|
|
8
|
+
const keyFor = (text, languageId) => `${languageId}\n${text}`;
|
|
9
|
+
const remember = (key, tokens) => {
|
|
10
|
+
if (settled.size >= MAX_CACHED_SNIPPETS) settled.delete(settled.keys().next().value);
|
|
11
|
+
settled.set(key, tokens);
|
|
12
|
+
};
|
|
13
|
+
const tokenize = (text, languageId) => {
|
|
14
|
+
const key = keyFor(text, languageId);
|
|
15
|
+
const known = settled.get(key);
|
|
16
|
+
if (known) return Promise.resolve(known);
|
|
17
|
+
const running = inFlight.get(key);
|
|
18
|
+
if (running) return running;
|
|
19
|
+
const request = feature.tokenize(text, languageId).catch(() => []).then((tokens) => {
|
|
20
|
+
inFlight.delete(key);
|
|
21
|
+
remember(key, tokens);
|
|
22
|
+
return tokens;
|
|
23
|
+
});
|
|
24
|
+
inFlight.set(key, request);
|
|
25
|
+
return request;
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
cached: (text, languageId) => settled.get(keyFor(text, languageId)) ?? null,
|
|
29
|
+
tokenize,
|
|
30
|
+
prepare: (blocks) => {
|
|
31
|
+
const waiting = [];
|
|
32
|
+
for (const block of blocks) {
|
|
33
|
+
const key = keyFor(block.text, block.languageId);
|
|
34
|
+
if (settled.has(key) || heldOnce.has(key)) continue;
|
|
35
|
+
heldOnce.add(key);
|
|
36
|
+
waiting.push(tokenize(block.text, block.languageId));
|
|
37
|
+
}
|
|
38
|
+
if (waiting.length === 0) return null;
|
|
39
|
+
return Promise.race([Promise.all(waiting), holdTimeout()]).then(() => void 0);
|
|
40
|
+
},
|
|
41
|
+
clear: () => {
|
|
42
|
+
settled.clear();
|
|
43
|
+
heldOnce.clear();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function holdTimeout() {
|
|
48
|
+
return new Promise((resolve) => setTimeout(resolve, TOKEN_HOLD_MS));
|
|
49
|
+
}
|
|
50
|
+
function paintCodeTokens(document, code, text, tokens) {
|
|
51
|
+
if (tokens.length === 0) return;
|
|
52
|
+
const nodes = [];
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
for (const token of tokens) {
|
|
55
|
+
const start = Math.max(token.start, cursor);
|
|
56
|
+
const end = Math.min(token.end, text.length);
|
|
57
|
+
if (end <= start) continue;
|
|
58
|
+
if (start > cursor) nodes.push(document.createTextNode(text.slice(cursor, start)));
|
|
59
|
+
nodes.push(tokenElement(document, text.slice(start, end), token));
|
|
60
|
+
cursor = end;
|
|
61
|
+
}
|
|
62
|
+
if (cursor < text.length) nodes.push(document.createTextNode(text.slice(cursor)));
|
|
63
|
+
code.replaceChildren(...nodes);
|
|
64
|
+
}
|
|
65
|
+
function tokenElement(document, text, token) {
|
|
66
|
+
const element = document.createElement("span");
|
|
67
|
+
element.textContent = text;
|
|
68
|
+
const { color, fontStyle, fontWeight, textDecoration } = token.style;
|
|
69
|
+
if (color) element.style.color = color;
|
|
70
|
+
if (fontStyle) element.style.fontStyle = fontStyle;
|
|
71
|
+
if (fontWeight !== void 0) element.style.fontWeight = String(fontWeight);
|
|
72
|
+
if (textDecoration) element.style.textDecoration = textDecoration;
|
|
73
|
+
return element;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { createTooltipCodeTokenizer, paintCodeTokens };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=codeTokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeTokens.js","names":[],"sources":["../src/codeTokens.ts"],"sourcesContent":["import type { EditorSnippetTokensFeature } from '@singapore-editor/core/extensions'\nimport type { EditorToken } from '@singapore-editor/core/syntax'\n\nconst MAX_CACHED_SNIPPETS = 64\n\n// Long enough for a worker round trip behind the document's own work, short enough that a hover\n// held for it never reads as late.\nconst TOKEN_HOLD_MS = 250\n\nexport type TooltipCodeBlock = {\n readonly text: string\n readonly languageId: string\n}\n\n/**\n * The editor's own tokens for a hover's fenced code. Settled answers are kept because a hover\n * re-renders whole as each participant reports, and a block that repainted from plain on every\n * render would flash. Tokens can carry a theme's literal colours, so a theme change clears them.\n */\nexport type TooltipCodeTokenizer = {\n cached(text: string, languageId: string): readonly EditorToken[] | null\n tokenize(text: string, languageId: string): Promise<readonly EditorToken[]>\n /**\n * Null when every block can paint in colour now, or was already waited for once. Otherwise\n * settles when they can or the hold runs out, so a hover opens coloured instead of turning.\n */\n prepare(blocks: readonly TooltipCodeBlock[]): Promise<void> | null\n clear(): void\n}\n\nexport function createTooltipCodeTokenizer(\n feature: Pick<EditorSnippetTokensFeature, 'tokenize'>,\n): TooltipCodeTokenizer {\n const settled = new Map<string, readonly EditorToken[]>()\n const inFlight = new Map<string, Promise<readonly EditorToken[]>>()\n const heldOnce = new Set<string>()\n const keyFor = (text: string, languageId: string): string => `${languageId}\\n${text}`\n\n const remember = (key: string, tokens: readonly EditorToken[]): void => {\n if (settled.size >= MAX_CACHED_SNIPPETS) settled.delete(settled.keys().next().value!)\n settled.set(key, tokens)\n }\n\n const tokenize = (text: string, languageId: string): Promise<readonly EditorToken[]> => {\n const key = keyFor(text, languageId)\n const known = settled.get(key)\n if (known) return Promise.resolve(known)\n\n const running = inFlight.get(key)\n if (running) return running\n\n // A failed parse settles as plain, so a broken grammar is not asked again on every render.\n const request = feature\n .tokenize(text, languageId)\n .catch((): readonly EditorToken[] => [])\n .then((tokens) => {\n inFlight.delete(key)\n remember(key, tokens)\n return tokens\n })\n inFlight.set(key, request)\n return request\n }\n\n return {\n cached: (text, languageId) => settled.get(keyFor(text, languageId)) ?? null,\n tokenize,\n prepare: (blocks) => {\n const waiting: Promise<unknown>[] = []\n for (const block of blocks) {\n const key = keyFor(block.text, block.languageId)\n if (settled.has(key) || heldOnce.has(key)) continue\n\n heldOnce.add(key)\n waiting.push(tokenize(block.text, block.languageId))\n }\n if (waiting.length === 0) return null\n\n return Promise.race([Promise.all(waiting), holdTimeout()]).then(() => undefined)\n },\n clear: () => {\n settled.clear()\n heldOnce.clear()\n },\n }\n}\n\nfunction holdTimeout(): Promise<void> {\n // @justification Caps how long a hover waits for tokens before painting plain; it only resolves\n // the losing side of a race.\n return new Promise((resolve) => setTimeout(resolve, TOKEN_HOLD_MS))\n}\n\nexport function paintCodeTokens(\n document: Document,\n code: HTMLElement,\n text: string,\n tokens: readonly EditorToken[],\n): void {\n if (tokens.length === 0) return\n\n const nodes: Node[] = []\n let cursor = 0\n for (const token of tokens) {\n const start = Math.max(token.start, cursor)\n const end = Math.min(token.end, text.length)\n if (end <= start) continue\n\n if (start > cursor) nodes.push(document.createTextNode(text.slice(cursor, start)))\n nodes.push(tokenElement(document, text.slice(start, end), token))\n cursor = end\n }\n if (cursor < text.length) nodes.push(document.createTextNode(text.slice(cursor)))\n code.replaceChildren(...nodes)\n}\n\nfunction tokenElement(document: Document, text: string, token: EditorToken): HTMLElement {\n const element = document.createElement('span')\n element.textContent = text\n const { color, fontStyle, fontWeight, textDecoration } = token.style\n if (color) element.style.color = color\n if (fontStyle) element.style.fontStyle = fontStyle\n if (fontWeight !== undefined) element.style.fontWeight = String(fontWeight)\n if (textDecoration) element.style.textDecoration = textDecoration\n return element\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EditorViewContributionContext, EditorViewContributionUpdateKind, EditorViewSnapshot } from '@singapore-editor/core/extensions';
|
|
2
|
+
export type HoverControllerOptions = {
|
|
3
|
+
readonly context: EditorViewContributionContext;
|
|
4
|
+
readonly classNamespace?: string;
|
|
5
|
+
readonly markdownCodeBackground?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type HoverController = {
|
|
8
|
+
update(snapshot: EditorViewSnapshot, kind: EditorViewContributionUpdateKind): void;
|
|
9
|
+
/** Shows the hover at an offset right away, as a keyboard summons does. */
|
|
10
|
+
showAtOffset(offset: number, options?: {
|
|
11
|
+
readonly focus?: boolean;
|
|
12
|
+
}): boolean;
|
|
13
|
+
hide(): void;
|
|
14
|
+
containsTarget(target: EventTarget | null): boolean;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The one hover an editor view has.
|
|
19
|
+
*
|
|
20
|
+
* Every plugin with something to say at a position registers an EditorHoverParticipant; this asks
|
|
21
|
+
* all of them on one clock and paints their answers in one surface. Two plugins can therefore never
|
|
22
|
+
* put two hovers on screen, and neither can open on a different delay than the other: the timing
|
|
23
|
+
* is not theirs to set.
|
|
24
|
+
*
|
|
25
|
+
* The clock is VS Code's. With a 300ms delay, async work starts at 150ms so a server answer
|
|
26
|
+
* overlaps the rest of the dwell, sync work runs and the surface paints at 300ms, and a hover still
|
|
27
|
+
* waiting on a server at 900ms shows a loading row.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createHoverController(options: HoverControllerOptions): HoverController;
|
|
30
|
+
//# sourceMappingURL=hoverController.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hoverController.d.ts","sourceRoot":"","sources":["../src/hoverController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gCAAgC,EAChC,kBAAkB,EACnB,MAAM,mCAAmC,CAAA;AAwB1C,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,6BAA6B,CAAA;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,OAAO,CAAA;CAC1C,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,CAAC,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,gCAAgC,GAAG,IAAI,CAAA;IAClF,2EAA2E;IAC3E,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IAC7E,IAAI,IAAI,IAAI,CAAA;IACZ,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAA;IACnD,OAAO,IAAI,IAAI,CAAA;CAChB,CAAA;AAwBD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAiPtF"}
|